Passed
Pull Request — master (#72)
by Daniel
06:31
created

AbstractUserEmailFactory::getContextKeys()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Factory\User\Mailer;
15
16
use Psr\Container\ContainerInterface;
17
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
18
use Silverback\ApiComponentsBundle\Event\UserEmailMessageEvent;
19
use Silverback\ApiComponentsBundle\Exception\BadMethodCallException;
20
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
21
use Silverback\ApiComponentsBundle\Exception\RfcComplianceException;
22
use Silverback\ApiComponentsBundle\Exception\UnexpectedValueException;
23
use Silverback\ApiComponentsBundle\Helper\RefererUrlResolver;
24
use Silverback\ApiComponentsBundle\Security\TokenGenerator;
25
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
26
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
27
use Symfony\Component\HttpFoundation\RequestStack;
28
use Symfony\Component\Mime\Address;
29
use Symfony\Component\Mime\Exception\RfcComplianceException as SymfonyRfcComplianceException;
30
use Symfony\Component\Mime\RawMessage;
31
use Symfony\Contracts\Service\ServiceSubscriberInterface;
32
use Twig\Environment;
33
34
/**
35
 * @author Daniel West <[email protected]>
36
 */
37
abstract class AbstractUserEmailFactory implements ServiceSubscriberInterface
38
{
39
    public const MESSAGE_ID_PREFIX = 'xx';
40
41
    protected ContainerInterface $container;
42
    private EventDispatcherInterface $eventDispatcher;
43
    protected string $subject;
44
    protected bool $enabled;
45
    protected ?string $defaultRedirectPath;
46
    protected ?string $redirectPathQueryKey;
47
    protected array $emailContext;
48
    protected ?RawMessage $message;
49
    private AbstractUser $user;
50
51 27
    public function __construct(ContainerInterface $container, EventDispatcherInterface $eventDispatcher, string $subject, bool $enabled = true, ?string $defaultRedirectPath = null, ?string $redirectPathQueryKey = null, array $emailContext = [])
52
    {
53 27
        $this->container = $container;
54 27
        $this->eventDispatcher = $eventDispatcher;
55 27
        $this->subject = $subject;
56 27
        $this->enabled = $enabled;
57 27
        $this->emailContext = $emailContext;
58 27
        $this->defaultRedirectPath = $defaultRedirectPath;
59 27
        $this->redirectPathQueryKey = $redirectPathQueryKey;
60 27
    }
61
62 2
    public static function getSubscribedServices(): array
63
    {
64
        return [
65 2
            RequestStack::class,
66
            RefererUrlResolver::class,
67
            Environment::class,
68
        ];
69
    }
70
71 7
    protected static function getContextKeys(): ?array
72
    {
73
        return [
74 7
            'website_name',
75
            'user',
76
        ];
77
    }
78
79 15
    protected function initUser(AbstractUser $user): void
80
    {
81 15
        if (!$user->getUsername()) {
82 1
            throw new InvalidArgumentException('The user must have a username set to send them any email');
83
        }
84
85 14
        if (!$user->getEmailAddress()) {
86 1
            throw new InvalidArgumentException('The user must have an email address set to send them any email');
87
        }
88
89 13
        $this->user = $user;
90 13
    }
91
92 12
    protected function createEmailMessage(array $context = []): ?TemplatedEmail
93
    {
94 12
        if (!$this->enabled) {
95 1
            return null;
96
        }
97
98 11
        if (!isset($this->user)) {
99 1
            throw new BadMethodCallException('You must call the method `initUser` before `createEmailMessage`');
100
        }
101
102
        try {
103 10
            $toEmailAddress = Address::fromString((string) $this->user->getEmailAddress());
104 1
        } catch (SymfonyRfcComplianceException $exception) {
105 1
            $exception = new RfcComplianceException($exception->getMessage());
106 1
            throw $exception;
107
        }
108
109 9
        $context = array_replace_recursive([
110 9
            'user' => $this->user,
111 9
        ], $this->emailContext, $context);
112 9
        $this->validateContext($context);
113
114 8
        $twig = $this->container->get(Environment::class);
115 8
        $template = $twig->createTemplate($this->subject);
116 8
        $subject = $template->render($context);
117
118 8
        $email = (new TemplatedEmail())
119 8
            ->to($toEmailAddress)
120 8
            ->subject($subject)
121 8
            ->htmlTemplate('@SilverbackApiComponents/emails/' . $this->getTemplate())
122 8
            ->context($context);
123
124 8
        $event = new UserEmailMessageEvent(static::class, $email);
125 8
        $this->eventDispatcher->dispatch($event);
126
127 8
        $email->getHeaders()->addTextHeader('X-Message-ID', sprintf('%s-%s', static::MESSAGE_ID_PREFIX, TokenGenerator::generateToken()));
128
129 8
        return $event->getEmail();
130
    }
131
132 8
    protected function getTokenUrl(string $token, string $username, ?string $newEmail = null): string
133
    {
134 8
        $path = $this->populatePathVariables($this->getTokenPath(), [
135 6
            'token' => $token,
136 6
            'username' => $username,
137 6
            'new_email' => $newEmail,
138
        ]);
139
140 6
        $refererUrlResolver = $this->container->get(RefererUrlResolver::class);
141
142 6
        return $refererUrlResolver->getAbsoluteUrl($path);
143
    }
144
145 8
    private function getTokenPath(): string
146
    {
147 8
        if (null === $this->defaultRedirectPath && null === $this->redirectPathQueryKey) {
148 1
            throw new InvalidArgumentException('The `defaultRedirectPath` or `redirectPathQueryKey` must be set');
149
        }
150
151 7
        $requestStack = $this->container->get(RequestStack::class);
152 7
        $request = $requestStack->getMasterRequest();
153
154 7
        $path = ($request && $this->redirectPathQueryKey) ?
155 3
            $request->query->get($this->redirectPathQueryKey, $this->defaultRedirectPath) :
156 7
            $this->defaultRedirectPath;
157
158 7
        if (null === $path) {
159 1
            throw new UnexpectedValueException(sprintf('The querystring key `%s` could not be found in the request to generate a token URL', $this->redirectPathQueryKey));
160
        }
161
162 6
        return $path;
163
    }
164
165 6
    private function populatePathVariables(string $path, array $variables): string
166
    {
167 6
        preg_match_all('/{{[\s]*(\w+)[\s]*}}/', $path, $matches);
168 6
        foreach ($matches[0] as $matchIndex => $fullMatch) {
169 1
            if (\array_key_exists($varKey = $matches[1][$matchIndex], $variables) && null !== $variables[$varKey]) {
170 1
                $path = str_replace($fullMatch, rawurlencode($variables[$varKey]), $path);
171
            }
172
        }
173
174 6
        return $path;
175
    }
176
177 9
    private function validateContext(array $context): void
178
    {
179 9
        $contextKeys = static::getContextKeys();
180 9
        $keys = array_keys($context);
181 9
        if (\count($differences = array_diff($contextKeys, $keys))) {
182 1
            throw new InvalidArgumentException(sprintf('You have not specified required context key(s) for the user email factory factory `%s` (expected: `%s`)', static::class, implode('`, `', $differences)));
183
        }
184 8
    }
185
186
    abstract public function create(AbstractUser $user, array $context = []): ?RawMessage;
187
188
    abstract protected function getTemplate(): string;
189
}
190