1 | <?php |
||||
2 | |||||
3 | namespace LeKoala\EmailTemplates\Email; |
||||
4 | |||||
5 | use Exception; |
||||
6 | use BadMethodCallException; |
||||
7 | use LeKoala\EmailTemplates\Extensions\EmailTemplateSiteConfigExtension; |
||||
8 | use SilverStripe\i18n\i18n; |
||||
9 | use SilverStripe\Control\HTTP; |
||||
10 | use SilverStripe\View\SSViewer; |
||||
11 | use SilverStripe\ORM\DataObject; |
||||
12 | use SilverStripe\Security\Member; |
||||
13 | use SilverStripe\Control\Director; |
||||
14 | use SilverStripe\View\Requirements; |
||||
15 | use SilverStripe\Control\Email\Email; |
||||
16 | use SilverStripe\SiteConfig\SiteConfig; |
||||
17 | use LeKoala\EmailTemplates\Models\SentEmail; |
||||
18 | use LeKoala\EmailTemplates\Helpers\EmailUtils; |
||||
19 | use LeKoala\EmailTemplates\Models\EmailTemplate; |
||||
0 ignored issues
–
show
|
|||||
20 | use LeKoala\EmailTemplates\Helpers\SubsiteHelper; |
||||
21 | use SilverStripe\Core\Injector\Injector; |
||||
22 | use SilverStripe\Security\DefaultAdminService; |
||||
23 | use SilverStripe\View\ArrayData; |
||||
24 | use SilverStripe\View\ViewableData; |
||||
25 | use Symfony\Component\Mailer\Exception\TransportExceptionInterface; |
||||
26 | use Symfony\Component\Mailer\MailerInterface; |
||||
27 | use Symfony\Component\Mime\Part\AbstractPart; |
||||
28 | |||||
29 | /** |
||||
30 | * An improved and more pleasant base Email class to use on your project |
||||
31 | * |
||||
32 | * This class is fully decoupled from the EmailTemplate class and keep be used |
||||
33 | * independantly |
||||
34 | * |
||||
35 | * Improvements are: |
||||
36 | * |
||||
37 | * - URL safe rewriting |
||||
38 | * - Configurable base template (base system use Email class with setHTMLTemplate to provide content) |
||||
39 | * - Send email according to member locale |
||||
40 | * - Check for subject |
||||
41 | * - Send to member or admin |
||||
42 | * - Persist emails |
||||
43 | * - Parse body (multi part body is supported) |
||||
44 | * - Plaintext takes template into account |
||||
45 | * - Disable emails |
||||
46 | * - Unified send methods that support hooks |
||||
47 | * |
||||
48 | * @author lekoala |
||||
49 | */ |
||||
50 | class BetterEmail extends Email |
||||
51 | { |
||||
52 | const STATE_CANCELLED = 'cancelled'; |
||||
53 | const STATE_NOT_SENT = 'not_sent'; |
||||
54 | const STATE_SENT = 'sent'; |
||||
55 | const STATE_FAILED = 'failed'; |
||||
56 | |||||
57 | /** |
||||
58 | * @var EmailTemplate|null |
||||
59 | */ |
||||
60 | protected $emailTemplate; |
||||
61 | |||||
62 | /** |
||||
63 | * @var string |
||||
64 | */ |
||||
65 | protected $locale; |
||||
66 | |||||
67 | /** |
||||
68 | * @var string |
||||
69 | */ |
||||
70 | protected $to; |
||||
71 | |||||
72 | /** |
||||
73 | * @var Member|null |
||||
74 | */ |
||||
75 | protected $to_member; |
||||
76 | |||||
77 | /** |
||||
78 | * @var string |
||||
79 | */ |
||||
80 | protected $from; |
||||
81 | |||||
82 | /** |
||||
83 | * @var Member|null |
||||
84 | */ |
||||
85 | protected $from_member; |
||||
86 | |||||
87 | /** |
||||
88 | * @var boolean |
||||
89 | */ |
||||
90 | protected $disabled = false; |
||||
91 | |||||
92 | /** |
||||
93 | * @var SentEmail|null |
||||
94 | */ |
||||
95 | protected $sentMail = null; |
||||
96 | |||||
97 | /** |
||||
98 | * @var boolean |
||||
99 | */ |
||||
100 | protected $sendingCancelled = false; |
||||
101 | |||||
102 | /** |
||||
103 | * Additional data available in a template. |
||||
104 | * Used in the same way than {@link ViewableData->customize()}. |
||||
105 | */ |
||||
106 | private ViewableData $data; |
||||
107 | |||||
108 | private bool $dataHasBeenSet = false; |
||||
109 | |||||
110 | /** |
||||
111 | * Email constructor. |
||||
112 | * @param string|array $from |
||||
113 | * @param string|array $to |
||||
114 | * @param string $subject |
||||
115 | * @param string $body |
||||
116 | * @param string|array $cc |
||||
117 | * @param string|array $bcc |
||||
118 | * @param string $returnPath |
||||
119 | */ |
||||
120 | public function __construct( |
||||
121 | string|array $from = '', |
||||
122 | string|array $to = '', |
||||
123 | string $subject = '', |
||||
124 | string $body = '', |
||||
125 | string|array $cc = '', |
||||
126 | string|array $bcc = '', |
||||
127 | string $returnPath = '' |
||||
128 | ) { |
||||
129 | parent::__construct($from, $to, $subject, $body, $cc, $bcc, $returnPath); |
||||
130 | |||||
131 | // Use template as a layout |
||||
132 | if ($defaultTemplate = self::config()->get('template')) { |
||||
133 | // Call method because variable is private |
||||
134 | parent::setHTMLTemplate($defaultTemplate); |
||||
135 | } |
||||
136 | $this->data = ViewableData::create(); |
||||
137 | } |
||||
138 | |||||
139 | /** |
||||
140 | * Persists the email to the database |
||||
141 | * |
||||
142 | * @param bool|array|string $results |
||||
143 | * @return SentEmail |
||||
144 | */ |
||||
145 | protected function persist($results) |
||||
146 | { |
||||
147 | $record = SentEmail::create([ |
||||
148 | 'To' => EmailUtils::format_email_addresses($this->getTo()), |
||||
149 | 'From' => EmailUtils::format_email_addresses($this->getFrom()), |
||||
150 | 'ReplyTo' => EmailUtils::format_email_addresses($this->getReplyTo()), |
||||
151 | 'Subject' => $this->getSubject(), |
||||
152 | 'Body' => $this->getRenderedBody(), |
||||
153 | 'Headers' => $this->getHeaders()->toString(), |
||||
154 | 'CC' => EmailUtils::format_email_addresses($this->getCC()), |
||||
155 | 'BCC' => EmailUtils::format_email_addresses($this->getBCC()), |
||||
156 | 'Results' => json_encode($results), |
||||
157 | ]); |
||||
158 | $record->write(); |
||||
159 | |||||
160 | // TODO: migrate this to a cron task |
||||
161 | SentEmail::cleanup(); |
||||
162 | |||||
163 | return $record; |
||||
164 | } |
||||
165 | |||||
166 | |||||
167 | /** |
||||
168 | * Get body of message after rendering |
||||
169 | * Useful for previews |
||||
170 | * |
||||
171 | * @return string |
||||
172 | */ |
||||
173 | public function getRenderedBody() |
||||
174 | { |
||||
175 | $this->render(); |
||||
176 | return $this->getHtmlBody(); |
||||
0 ignored issues
–
show
|
|||||
177 | } |
||||
178 | |||||
179 | /** |
||||
180 | * Don't forget that setBody will erase content of html template |
||||
181 | * Prefer to use this instead. Basically you can replace setBody calls with this method |
||||
182 | * URLs are rewritten by render process |
||||
183 | * |
||||
184 | * @param string $body |
||||
185 | * @return static |
||||
186 | */ |
||||
187 | public function addBody($body) |
||||
188 | { |
||||
189 | return $this->addData("EmailContent", $body); |
||||
190 | } |
||||
191 | |||||
192 | /** |
||||
193 | * @param string $body The email body |
||||
194 | * @return static |
||||
195 | */ |
||||
196 | public function setBody(AbstractPart|string $body = null): static |
||||
197 | { |
||||
198 | $this->text(null); |
||||
199 | |||||
200 | $body = self::rewriteURLs($body); |
||||
201 | parent::setBody($body); |
||||
202 | |||||
203 | return $this; |
||||
204 | } |
||||
205 | |||||
206 | /** |
||||
207 | * Get data which is exposed to the template |
||||
208 | * |
||||
209 | * The following data is exposed via this method by default: |
||||
210 | * IsEmail: used to detect if rendering an email template rather than a page template |
||||
211 | * BaseUrl: used to get the base URL for the email |
||||
212 | */ |
||||
213 | public function getData(): ViewableData |
||||
214 | { |
||||
215 | $extraData = [ |
||||
216 | 'IsEmail' => true, |
||||
217 | 'BaseURL' => Director::absoluteBaseURL(), |
||||
218 | ]; |
||||
219 | $data = clone $this->data; |
||||
220 | foreach ($extraData as $key => $value) { |
||||
221 | // @phpstan-ignore-next-line |
||||
222 | if (is_null($data->{$key})) { |
||||
223 | $data->{$key} = $value; |
||||
224 | } |
||||
225 | } |
||||
226 | $this->extend('updateGetData', $data); |
||||
227 | return $data; |
||||
228 | } |
||||
229 | |||||
230 | /** |
||||
231 | * Add data to be used in the template |
||||
232 | * |
||||
233 | * Calling addData() once means that any content set via text()/html()/setBody() will have no effect |
||||
234 | * |
||||
235 | * @param string|array $nameOrData can be either the name to add, or an array of [name => value] |
||||
236 | */ |
||||
237 | public function addData(string|array $nameOrData, mixed $value = null): static |
||||
238 | { |
||||
239 | if (is_array($nameOrData)) { |
||||
0 ignored issues
–
show
|
|||||
240 | foreach ($nameOrData as $key => $val) { |
||||
241 | $this->data->{$key} = $val; |
||||
242 | } |
||||
243 | } else { |
||||
244 | $this->data->{$nameOrData} = $value; |
||||
245 | } |
||||
246 | $this->dataHasBeenSet = true; |
||||
247 | return $this; |
||||
248 | } |
||||
249 | |||||
250 | /** |
||||
251 | * Remove a single piece of template data |
||||
252 | */ |
||||
253 | public function removeData(string $name) |
||||
254 | { |
||||
255 | $this->data->{$name} = null; |
||||
256 | return $this; |
||||
257 | } |
||||
258 | |||||
259 | /** |
||||
260 | * @param array|ViewableData $data The template data to set |
||||
261 | * @return $this |
||||
262 | */ |
||||
263 | public function setData($data) |
||||
264 | { |
||||
265 | // Merge data! |
||||
266 | if ($this->emailTemplate) { |
||||
267 | if (is_array($data)) { |
||||
268 | $this->setDataInternal($data); |
||||
269 | } elseif ($data instanceof DataObject) { |
||||
270 | $this->setDataInternal($data->toMap()); |
||||
271 | } else { |
||||
272 | $this->setDataInternal($data); |
||||
273 | } |
||||
274 | } else { |
||||
275 | $this->setDataInternal($data); |
||||
276 | } |
||||
277 | return $this; |
||||
278 | } |
||||
279 | |||||
280 | /** |
||||
281 | * Set template data |
||||
282 | * |
||||
283 | * Calling setData() once means that any content set via text()/html()/setBody() will have no effect |
||||
284 | */ |
||||
285 | protected function setDataInternal(array|ViewableData $data) |
||||
286 | { |
||||
287 | if (is_array($data)) { |
||||
0 ignored issues
–
show
|
|||||
288 | $data = ArrayData::create($data); |
||||
289 | } |
||||
290 | $this->data = $data; |
||||
291 | $this->dataHasBeenSet = true; |
||||
292 | return $this; |
||||
293 | } |
||||
294 | |||||
295 | /** |
||||
296 | * Sends a HTML email |
||||
297 | * |
||||
298 | * @return void |
||||
299 | */ |
||||
300 | public function send(): void |
||||
301 | { |
||||
302 | $this->doSend(false); |
||||
303 | } |
||||
304 | |||||
305 | /** |
||||
306 | * Sends a plain text email |
||||
307 | * |
||||
308 | * @return void |
||||
309 | */ |
||||
310 | public function sendPlain(): void |
||||
311 | { |
||||
312 | $this->doSend(true); |
||||
313 | } |
||||
314 | |||||
315 | /** |
||||
316 | * Send this email |
||||
317 | * |
||||
318 | * @param bool $plain |
||||
319 | * @return bool|string true if successful or error string on failure |
||||
320 | * @throws Exception |
||||
321 | */ |
||||
322 | public function doSend($plain = false) |
||||
323 | { |
||||
324 | if ($this->disabled) { |
||||
325 | $this->sendingCancelled = true; |
||||
326 | return false; |
||||
327 | } |
||||
328 | |||||
329 | // Check for Subject |
||||
330 | if (!$this->getSubject()) { |
||||
331 | throw new BadMethodCallException('You must set a subject'); |
||||
332 | } |
||||
333 | |||||
334 | // This hook can prevent email from being sent |
||||
335 | $result = $this->extend('onBeforeDoSend', $this); |
||||
336 | if ($result === false) { |
||||
0 ignored issues
–
show
|
|||||
337 | $this->sendingCancelled = true; |
||||
338 | return false; |
||||
339 | } |
||||
340 | |||||
341 | $SiteConfig = $this->currentSiteConfig(); |
||||
342 | |||||
343 | // Check for Sender and use default if necessary |
||||
344 | $from = $this->getFrom(); |
||||
345 | if (empty($from)) { |
||||
346 | $this->setFrom($SiteConfig->EmailDefaultSender()); |
||||
347 | } |
||||
348 | |||||
349 | // Check for Recipient and use default if necessary |
||||
350 | $to = $this->getTo(); |
||||
351 | if (empty($to)) { |
||||
352 | $this->addTo($SiteConfig->EmailDefaultRecipient()); |
||||
353 | } |
||||
354 | |||||
355 | // Set language to use for the email |
||||
356 | $restore_locale = null; |
||||
357 | if ($this->locale) { |
||||
358 | $restore_locale = i18n::get_locale(); |
||||
359 | i18n::set_locale($this->locale); |
||||
360 | } |
||||
361 | |||||
362 | $member = $this->to_member; |
||||
363 | if ($member) { |
||||
364 | // Maybe this member doesn't want to receive emails? |
||||
365 | // @phpstan-ignore-next-line |
||||
366 | if ($member->hasMethod('canReceiveEmails') && !$member->canReceiveEmails()) { |
||||
367 | return false; |
||||
368 | } |
||||
369 | } |
||||
370 | |||||
371 | // Make sure we have a full render with current locale |
||||
372 | if ($this->emailTemplate) { |
||||
373 | $this->clearBody(); |
||||
374 | } |
||||
375 | |||||
376 | try { |
||||
377 | $res = true; |
||||
378 | if ($plain) { |
||||
379 | $this->internalSendPlain(); |
||||
380 | } else { |
||||
381 | $this->internalSend(); |
||||
382 | } |
||||
383 | } catch (TransportExceptionInterface $th) { |
||||
384 | $res = $th->getMessage(); |
||||
385 | } |
||||
386 | |||||
387 | if ($restore_locale) { |
||||
388 | i18n::set_locale($restore_locale); |
||||
389 | } |
||||
390 | |||||
391 | $this->extend('onAfterDoSend', $this, $res); |
||||
392 | $this->sentMail = $this->persist($res); |
||||
393 | |||||
394 | return $res; |
||||
395 | } |
||||
396 | |||||
397 | private function internalSendPlain() |
||||
398 | { |
||||
399 | $html = $this->getHtmlBody(); |
||||
400 | $this->render(true); |
||||
401 | $this->html(null); |
||||
402 | Injector::inst()->get(MailerInterface::class)->send($this); |
||||
403 | $this->html($html); |
||||
404 | } |
||||
405 | |||||
406 | private function internalSend() |
||||
407 | { |
||||
408 | $this->render(); |
||||
409 | Injector::inst()->get(MailerInterface::class)->send($this); |
||||
410 | } |
||||
411 | |||||
412 | /** |
||||
413 | * Returns one of the STATE_xxxx constant |
||||
414 | * |
||||
415 | * @return string |
||||
416 | */ |
||||
417 | public function getSendStatus() |
||||
418 | { |
||||
419 | if ($this->sendingCancelled) { |
||||
420 | return self::STATE_CANCELLED; |
||||
421 | } |
||||
422 | if ($this->sentMail) { |
||||
423 | if ($this->sentMail->IsSuccess()) { |
||||
424 | return self::STATE_SENT; |
||||
425 | } |
||||
426 | return self::STATE_FAILED; |
||||
427 | } |
||||
428 | return self::STATE_NOT_SENT; |
||||
429 | } |
||||
430 | |||||
431 | /** |
||||
432 | * Was sending cancelled ? |
||||
433 | * |
||||
434 | * @return bool |
||||
435 | */ |
||||
436 | public function getSendingCancelled() |
||||
437 | { |
||||
438 | return $this->sendingCancelled; |
||||
439 | } |
||||
440 | |||||
441 | /** |
||||
442 | * The last result from "send" method. Null if not sent yet or sending was cancelled |
||||
443 | * |
||||
444 | * @return SentEmail |
||||
445 | */ |
||||
446 | public function getSentMail() |
||||
447 | { |
||||
448 | return $this->sentMail; |
||||
449 | } |
||||
450 | |||||
451 | /** |
||||
452 | * @return $this |
||||
453 | */ |
||||
454 | public function clearBody() |
||||
455 | { |
||||
456 | $this->setBody(null); |
||||
457 | return $this; |
||||
458 | } |
||||
459 | |||||
460 | /** |
||||
461 | * Set the template to render the email with |
||||
462 | * |
||||
463 | * This method is overidden in order to look for email templates to provide |
||||
464 | * content to |
||||
465 | * |
||||
466 | * @param string $template |
||||
467 | * @return static |
||||
468 | */ |
||||
469 | public function setHTMLTemplate(string $template): static |
||||
470 | { |
||||
471 | if (substr($template, -3) == '.ss') { |
||||
472 | $template = substr($template, 0, -3); |
||||
473 | } |
||||
474 | |||||
475 | // Do we have a custom template matching this code? |
||||
476 | $code = self::makeTemplateCode($template); |
||||
477 | $emailTemplate = EmailTemplate::getByCode($code, false); |
||||
478 | if ($emailTemplate) { |
||||
479 | $emailTemplate->applyTemplate($this); |
||||
480 | return $this; |
||||
481 | } |
||||
482 | |||||
483 | // If not, keep default behaviour (call method because var is private) |
||||
484 | return parent::setHTMLTemplate($template); |
||||
0 ignored issues
–
show
|
|||||
485 | } |
||||
486 | |||||
487 | /** |
||||
488 | * Make a template code |
||||
489 | * |
||||
490 | * @param string $str |
||||
491 | * @return string |
||||
492 | */ |
||||
493 | public static function makeTemplateCode($str) |
||||
494 | { |
||||
495 | // If we get a class name |
||||
496 | $parts = explode('\\', $str); |
||||
497 | $str = end($parts); |
||||
498 | $code = preg_replace('/Email$/', '', $str); |
||||
499 | return $code; |
||||
500 | } |
||||
501 | |||||
502 | /** |
||||
503 | * Helper method to render string with data |
||||
504 | * |
||||
505 | * @param string $content |
||||
506 | * @return string |
||||
507 | */ |
||||
508 | public function renderWithData($content) |
||||
509 | { |
||||
510 | $viewer = SSViewer::fromString($content); |
||||
0 ignored issues
–
show
The function
SilverStripe\View\SSViewer::fromString() has been deprecated: 5.4.0 Will be replaced with SilverStripe\TemplateEngine\SSTemplateEngine::renderString()
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
511 | $data = $this->getData(); |
||||
512 | |||||
513 | $result = (string) $viewer->process($data); |
||||
514 | $result = self::rewriteURLs($result); |
||||
515 | return $result; |
||||
516 | } |
||||
517 | |||||
518 | /** |
||||
519 | * Call html() and/or text() after rendering email templates |
||||
520 | * If either body html or text were previously explicitly set, those values will not be overwritten |
||||
521 | * |
||||
522 | * @param bool $plainOnly - if true then do not call html() |
||||
523 | */ |
||||
524 | public function render(bool $plainOnly = false): void |
||||
525 | { |
||||
526 | // Respect explicitly set body |
||||
527 | $htmlBody = $plainBody = null; |
||||
528 | |||||
529 | // Only respect if we don't have an email template |
||||
530 | if ($this->emailTemplate) { |
||||
531 | $htmlBody = $plainOnly ? null : $this->getHtmlBody(); |
||||
532 | $plainBody = $plainOnly ? $this->getTextBody() : null; |
||||
533 | } |
||||
534 | |||||
535 | // Ensure we can at least render something |
||||
536 | $htmlTemplate = $this->getHTMLTemplate(); |
||||
537 | $plainTemplate = $this->getPlainTemplate(); |
||||
538 | if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) { |
||||
539 | return; |
||||
540 | } |
||||
541 | |||||
542 | $htmlRender = null; |
||||
543 | $plainRender = null; |
||||
544 | |||||
545 | if ($htmlBody && !$this->dataHasBeenSet) { |
||||
546 | $htmlRender = $htmlBody; |
||||
547 | } |
||||
548 | |||||
549 | if ($plainBody && !$this->dataHasBeenSet) { |
||||
550 | $plainRender = $plainBody; |
||||
551 | } |
||||
552 | |||||
553 | // Do not interfere with emails styles |
||||
554 | Requirements::clear(); |
||||
555 | |||||
556 | // Render plain |
||||
557 | if (!$plainRender && $plainTemplate) { |
||||
558 | $plainRender = $this->getData()->renderWith($plainTemplate)->Plain(); |
||||
559 | // Do another round of rendering to render our variables inside |
||||
560 | $plainRender = $this->renderWithData($plainRender); |
||||
561 | } |
||||
562 | |||||
563 | // Render HTML part, either if sending html email, or a plain part is lacking |
||||
564 | if (!$htmlRender && $htmlTemplate && (!$plainOnly || empty($plainRender))) { |
||||
565 | $htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW(); |
||||
566 | // Do another round of rendering to render our variables inside |
||||
567 | $htmlRender = $this->renderWithData($htmlRender); |
||||
568 | } |
||||
569 | |||||
570 | // Render subject with data as well |
||||
571 | $subject = $this->renderWithData($this->getSubject()); |
||||
572 | // Html entities in email titles is not a good idea |
||||
573 | $subject = html_entity_decode($subject, ENT_QUOTES | ENT_XML1, 'UTF-8'); |
||||
574 | // Avoid crazy template name in email |
||||
575 | $subject = preg_replace("/<!--(.)+-->/", "", $subject); |
||||
576 | parent::setSubject($subject); |
||||
577 | |||||
578 | // Plain render fallbacks to using the html render with html tags removed |
||||
579 | if (!$plainRender && $htmlRender) { |
||||
580 | $plainRender = EmailUtils::convert_html_to_text($htmlRender); |
||||
0 ignored issues
–
show
It seems like
$htmlRender can also be of type resource ; however, parameter $content of LeKoala\EmailTemplates\H...:convert_html_to_text() does only seem to accept Symfony\Component\Mime\Part\TextPart|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
581 | } |
||||
582 | |||||
583 | // Rendering is finished |
||||
584 | Requirements::restore(); |
||||
585 | |||||
586 | // Handle edge case where no template was found |
||||
587 | if (!$htmlRender && $htmlBody) { |
||||
588 | $htmlRender = $htmlBody; |
||||
589 | } |
||||
590 | |||||
591 | if (!$plainRender && $plainBody) { |
||||
592 | $plainRender = $plainBody; |
||||
593 | } |
||||
594 | |||||
595 | if ($plainRender) { |
||||
596 | $this->text($plainRender); |
||||
597 | } |
||||
598 | if ($htmlRender && !$plainOnly) { |
||||
599 | $this->html($htmlRender); |
||||
600 | } |
||||
601 | } |
||||
602 | |||||
603 | /** |
||||
604 | * Get locale set before email is sent |
||||
605 | * |
||||
606 | * @return string |
||||
607 | */ |
||||
608 | public function getLocale() |
||||
609 | { |
||||
610 | return $this->locale; |
||||
611 | } |
||||
612 | |||||
613 | /** |
||||
614 | * Set locale to set before email is sent |
||||
615 | * |
||||
616 | * @param string $val |
||||
617 | */ |
||||
618 | public function setLocale($val) |
||||
619 | { |
||||
620 | $this->locale = $val; |
||||
621 | } |
||||
622 | |||||
623 | /** |
||||
624 | * Is this email disabled ? |
||||
625 | * |
||||
626 | * @return boolean |
||||
627 | */ |
||||
628 | public function getDisabled() |
||||
629 | { |
||||
630 | return $this->disabled; |
||||
631 | } |
||||
632 | |||||
633 | /** |
||||
634 | * Disable this email (sending will have no effect) |
||||
635 | * |
||||
636 | * @param bool $disabled |
||||
637 | * @return $this |
||||
638 | */ |
||||
639 | public function setDisabled($disabled) |
||||
640 | { |
||||
641 | $this->disabled = (bool) $disabled; |
||||
642 | return $this; |
||||
643 | } |
||||
644 | |||||
645 | /** |
||||
646 | * Get recipient as member |
||||
647 | * |
||||
648 | * @return Member |
||||
649 | */ |
||||
650 | public function getToMember() |
||||
651 | { |
||||
652 | if (!$this->to_member && $this->to) { |
||||
653 | $email = EmailUtils::get_email_from_rfc_email($this->to); |
||||
654 | $member = Member::get()->filter(['Email' => $email])->first(); |
||||
655 | if ($member) { |
||||
656 | $this->setToMember($member); |
||||
657 | } |
||||
658 | } |
||||
659 | return $this->to_member; |
||||
660 | } |
||||
661 | |||||
662 | /** |
||||
663 | * Set recipient(s) of the email |
||||
664 | * |
||||
665 | * To send to many, pass an array: |
||||
666 | * array('[email protected]' => 'My Name', '[email protected]'); |
||||
667 | * |
||||
668 | * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name |
||||
669 | * @param string $name The name of the recipient (if one) |
||||
670 | * @return static |
||||
671 | */ |
||||
672 | public function setTo(string|array $address, string $name = ''): static |
||||
673 | { |
||||
674 | // Allow Name <[email protected]> |
||||
675 | if (!$name && is_string($address)) { |
||||
0 ignored issues
–
show
|
|||||
676 | $name = EmailUtils::get_displayname_from_rfc_email($address); |
||||
677 | $address = EmailUtils::get_email_from_rfc_email($address); |
||||
678 | } |
||||
679 | // Make sure this doesn't conflict with to_member property |
||||
680 | if ($this->to_member) { |
||||
681 | if (is_string($address)) { |
||||
0 ignored issues
–
show
|
|||||
682 | // We passed an email that doesn't match to member |
||||
683 | if ($this->to_member->Email != $address) { |
||||
684 | $this->to_member = null; |
||||
685 | } |
||||
686 | } else { |
||||
687 | $this->to_member = null; |
||||
688 | } |
||||
689 | } |
||||
690 | $this->to = $address; |
||||
0 ignored issues
–
show
It seems like
$address of type array is incompatible with the declared type string of property $to .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.. ![]() |
|||||
691 | return parent::setTo($address, $name); |
||||
0 ignored issues
–
show
|
|||||
692 | } |
||||
693 | |||||
694 | |||||
695 | |||||
696 | /** |
||||
697 | * @param string $subject The Subject line for the email |
||||
698 | * @return static |
||||
699 | */ |
||||
700 | public function setSubject(string $subject): static |
||||
701 | { |
||||
702 | // Do not allow changing subject if a template is set |
||||
703 | if ($this->emailTemplate && $this->getSubject()) { |
||||
704 | return $this; |
||||
705 | } |
||||
706 | return parent::setSubject($subject); |
||||
0 ignored issues
–
show
|
|||||
707 | } |
||||
708 | |||||
709 | /** |
||||
710 | * Send to admin |
||||
711 | * |
||||
712 | * @return Email |
||||
713 | */ |
||||
714 | public function setToAdmin() |
||||
715 | { |
||||
716 | $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin(); |
||||
717 | return $this->setToMember($admin); |
||||
718 | } |
||||
719 | |||||
720 | /** |
||||
721 | * Wrapper to report proper SiteConfig type |
||||
722 | * |
||||
723 | * @return SiteConfig|EmailTemplateSiteConfigExtension |
||||
724 | */ |
||||
725 | public function currentSiteConfig() |
||||
726 | { |
||||
727 | /** @var SiteConfig|EmailTemplateSiteConfigExtension */ |
||||
728 | return SiteConfig::current_site_config(); |
||||
729 | } |
||||
730 | /** |
||||
731 | * Set to |
||||
732 | * |
||||
733 | * @return Email |
||||
734 | */ |
||||
735 | public function setToContact() |
||||
736 | { |
||||
737 | $email = $this->currentSiteConfig()->EmailDefaultRecipient(); |
||||
738 | return $this->setTo($email); |
||||
739 | } |
||||
740 | |||||
741 | /** |
||||
742 | * Add in bcc admin |
||||
743 | * |
||||
744 | * @return Email |
||||
745 | */ |
||||
746 | public function bccToAdmin() |
||||
747 | { |
||||
748 | $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin(); |
||||
749 | return $this->addBCC($admin->Email); |
||||
750 | } |
||||
751 | |||||
752 | /** |
||||
753 | * Add in bcc admin |
||||
754 | * |
||||
755 | * @return Email |
||||
756 | */ |
||||
757 | public function bccToContact() |
||||
758 | { |
||||
759 | $email = $this->currentSiteConfig()->EmailDefaultRecipient(); |
||||
760 | return $this->addBCC($email); |
||||
761 | } |
||||
762 | |||||
763 | /** |
||||
764 | * Set a member as a recipient. |
||||
765 | * |
||||
766 | * It will also set the $Recipient variable in the template |
||||
767 | * |
||||
768 | * @param Member $member |
||||
769 | * @param string $locale Locale to use, set to false to keep current locale |
||||
770 | * @return BetterEmail |
||||
771 | */ |
||||
772 | public function setToMember(Member $member, $locale = null) |
||||
773 | { |
||||
774 | if ($locale === null) { |
||||
775 | $this->locale = $member->Locale; |
||||
776 | } else { |
||||
777 | $this->locale = $locale; |
||||
778 | } |
||||
779 | $this->to_member = $member; |
||||
780 | |||||
781 | $this->addData(['Recipient' => $member]); |
||||
782 | |||||
783 | return $this->setTo($member->Email, $member->getTitle()); |
||||
784 | } |
||||
785 | |||||
786 | /** |
||||
787 | * Get sender as member |
||||
788 | * |
||||
789 | * @return Member |
||||
790 | */ |
||||
791 | public function getFromMember() |
||||
792 | { |
||||
793 | if (!$this->from_member && $this->from) { |
||||
794 | $email = EmailUtils::get_email_from_rfc_email($this->from); |
||||
795 | $member = Member::get()->filter(['Email' => $email])->first(); |
||||
796 | if ($member) { |
||||
797 | $this->setFromMember($member); |
||||
798 | } |
||||
799 | } |
||||
800 | return $this->from_member; |
||||
801 | } |
||||
802 | |||||
803 | /** |
||||
804 | * Set From Member |
||||
805 | * |
||||
806 | * It will also set the $Sender variable in the template |
||||
807 | * |
||||
808 | * @param Member $member |
||||
809 | * @return BetterEmail |
||||
810 | */ |
||||
811 | public function setFromMember(Member $member) |
||||
812 | { |
||||
813 | $this->from_member = $member; |
||||
814 | |||||
815 | $this->addData(['Sender' => $member]); |
||||
816 | |||||
817 | return $this->setFrom($member->Email, $member->getTitle()); |
||||
818 | } |
||||
819 | |||||
820 | /** |
||||
821 | * Improved set from that supports Name <[email protected]> notation |
||||
822 | * |
||||
823 | * @param string|array $address |
||||
824 | * @param string $name |
||||
825 | * @return static |
||||
826 | */ |
||||
827 | public function setFrom(string|array $address, string $name = ''): static |
||||
828 | { |
||||
829 | if (!$name && is_string($address)) { |
||||
0 ignored issues
–
show
|
|||||
830 | $name = EmailUtils::get_displayname_from_rfc_email($address); |
||||
831 | $address = EmailUtils::get_email_from_rfc_email($address); |
||||
832 | } |
||||
833 | $this->from = $address; |
||||
0 ignored issues
–
show
It seems like
$address of type array is incompatible with the declared type string of property $from .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.. ![]() |
|||||
834 | return parent::setFrom($address, $name); |
||||
0 ignored issues
–
show
|
|||||
835 | } |
||||
836 | |||||
837 | /** |
||||
838 | * Bug safe absolute url that support subsites |
||||
839 | * |
||||
840 | * @param string $url |
||||
841 | * @param bool $relativeToSiteBase |
||||
842 | * @return string |
||||
843 | */ |
||||
844 | protected static function safeAbsoluteURL($url, $relativeToSiteBase = false) |
||||
845 | { |
||||
846 | if (empty($url)) { |
||||
847 | $absUrl = Director::baseURL(); |
||||
848 | } else { |
||||
849 | $firstCharacter = substr($url, 0, 1); |
||||
850 | |||||
851 | // It's a merge tag, don't touch it because we don't know what kind of url it contains |
||||
852 | if (in_array($firstCharacter, ['*', '$', '%'])) { |
||||
853 | return $url; |
||||
854 | } |
||||
855 | |||||
856 | $absUrl = Director::absoluteURL($url, $relativeToSiteBase ? Director::BASE : Director::ROOT); |
||||
857 | } |
||||
858 | |||||
859 | // If we use subsite, absolute url may not use the proper url |
||||
860 | $absUrl = SubsiteHelper::safeAbsoluteURL($absUrl); |
||||
861 | |||||
862 | return $absUrl; |
||||
0 ignored issues
–
show
The expression
return $absUrl could also return false which is incompatible with the documented return type string . Did you maybe forget to handle an error condition?
If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled. ![]() |
|||||
863 | } |
||||
864 | |||||
865 | /** |
||||
866 | * Turn all relative URLs in the content to absolute URLs |
||||
867 | */ |
||||
868 | protected static function rewriteURLs(AbstractPart|string $html = null) |
||||
869 | { |
||||
870 | if ($html instanceof AbstractPart) { |
||||
871 | $html = $html->bodyToString(); |
||||
872 | } |
||||
873 | if (isset($_SERVER['REQUEST_URI'])) { |
||||
874 | $html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html ?? ''); |
||||
875 | } |
||||
876 | return HTTP::urlRewriter($html, function ($url) { |
||||
0 ignored issues
–
show
It seems like
$html can also be of type Symfony\Component\Mime\Part\AbstractPart ; however, parameter $content of SilverStripe\Control\HTTP::urlRewriter() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
877 | //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":") |
||||
878 | if (preg_match('/^\w+:/', $url)) { |
||||
879 | return $url; |
||||
880 | } |
||||
881 | return self::safeAbsoluteURL($url, true); |
||||
882 | }); |
||||
883 | } |
||||
884 | |||||
885 | /** |
||||
886 | * Get the value of emailTemplate |
||||
887 | * @return EmailTemplate |
||||
888 | */ |
||||
889 | public function getEmailTemplate() |
||||
890 | { |
||||
891 | return $this->emailTemplate; |
||||
892 | } |
||||
893 | |||||
894 | /** |
||||
895 | * Set the value of emailTemplate |
||||
896 | * |
||||
897 | * @param EmailTemplate $emailTemplate |
||||
898 | * @return $this |
||||
899 | */ |
||||
900 | public function setEmailTemplate(EmailTemplate $emailTemplate) |
||||
901 | { |
||||
902 | $this->emailTemplate = $emailTemplate; |
||||
903 | return $this; |
||||
904 | } |
||||
905 | } |
||||
906 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths