Passed
Pull Request — master (#19)
by Sergey
02:27
created

EmailTemplate::canView()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace LeKoala\EmailTemplates\Models;
4
5
use Exception;
6
use SilverStripe\Forms\Tab;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\View\ArrayData;
11
use SilverStripe\Forms\TextField;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\Environment;
15
use SilverStripe\Forms\HeaderField;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Forms\LiteralField;
18
use SilverStripe\Control\Email\Email;
19
use SilverStripe\Forms\CheckboxField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\Security\Permission;
22
use SilverStripe\SiteConfig\SiteConfig;
23
use SilverStripe\Admin\AdminRootController;
24
use LeKoala\EmailTemplates\Email\BetterEmail;
25
use LeKoala\EmailTemplates\Helpers\FluentHelper;
26
use LeKoala\EmailTemplates\Admin\EmailTemplatesAdmin;
27
28
/**
29
 * User defined email templates
30
 *
31
 * Content of the template should override default content provided with setHTMLTemplate
32
 *
33
 * For example, in the framework we have
34
 *    $email = Email::create()->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
35
 *
36
 * It means our template code should match this : ForgotPasswordEmail
37
 *
38
 * @property string $Subject
39
 * @property string $DefaultSender
40
 * @property string $DefaultRecipient
41
 * @property string $Category
42
 * @property string $Code
43
 * @property string $Content
44
 * @property string $Callout
45
 * @property boolean $Disabled
46
 * @property int $SubsiteID
47
 * @author lekoala
48
 */
49
class EmailTemplate extends DataObject
50
{
51
    private static $table_name = 'EmailTemplate';
52
53
    private static $db = [
54
        'Subject' => 'Varchar(255)',
55
        'DefaultSender' => 'Varchar(255)',
56
        'DefaultRecipient' => 'Varchar(255)',
57
        'Category' => 'Varchar(255)',
58
        'Code' => 'Varchar(255)',
59
        // Content
60
        'Content' => 'HTMLText',
61
        'Callout' => 'HTMLText',
62
        // Configuration
63
        'Disabled' => 'Boolean',
64
    ];
65
    private static $summary_fields = [
66
        'Subject',
67
        'Code',
68
        'Category',
69
        'Disabled',
70
    ];
71
    private static $searchable_fields = [
72
        'Subject',
73
        'Code',
74
        'Category',
75
        'Disabled',
76
    ];
77
    private static $indexes = [
78
        'Code' => true, // Code is not unique because it can be used by subsites
79
    ];
80
    private static $translate = [
81
        'Subject', 'Content', 'Callout'
82
    ];
83
84
    public function getTitle()
85
    {
86
        return $this->Subject;
87
    }
88
89
    public function getCMSFields()
90
    {
91
        $fields = parent::getCMSFields();
92
93
        // Do not allow changing subsite
94
        $fields->removeByName('SubsiteID');
95
96
        $fields->dataFieldByName('Callout')->setRows(5);
97
98
        $codeField = $fields->dataFieldByName('Code');
99
        $codeField->setAttribute('placeholder', _t('EmailTemplate.CODEPLACEHOLDER', 'A unique code that will be used in code to retrieve the template, e.g.: MyEmail'));
100
101
        if ($this->Code) {
102
            $codeField->setReadonly(true);
103
        }
104
105
        // Merge fields helper
106
        $fields->addFieldToTab('Root.Main', new HeaderField('MergeFieldsHelperTitle', _t('EmailTemplate.AVAILABLEMERGEFIELDSTITLE', 'Available merge fields')));
107
108
        $fields->addFieldToTab('Root.Main', new LiteralField('MergeFieldsHelper', $this->mergeFieldsHelper()));
109
110
        if ($this->ID) {
111
            $fields->addFieldToTab('Root.Preview', $this->previewTab());
112
        }
113
114
        // Cleanup UI
115
        $categories = EmailTemplate::get()->column('Category');
116
        $fields->addFieldToTab('Root.Settings', new DropdownField('Category', 'Category', array_combine($categories, $categories)));
117
        $fields->addFieldToTab('Root.Settings', new CheckboxField('Disabled'));
118
        $fields->addFieldToTab('Root.Settings', new TextField('DefaultSender'));
119
        $fields->addFieldToTab('Root.Settings', new TextField('DefaultRecipient'));
120
121
122
        return $fields;
123
    }
124
125
    public function canView($member = null)
126
    {
127
        return true;
128
    }
129
130
    public function canEdit($member = null)
131
    {
132
        return Permission::check('CMS_ACCESS', 'any', $member);
133
    }
134
135
    public function canCreate($member = null, $context = [])
136
    {
137
        // Should be created by developer
138
        return false;
139
    }
140
141
    public function canDelete($member = null)
142
    {
143
        return Permission::check('CMS_ACCESS', 'any', $member);
144
    }
145
146
    /**
147
     * A map of Name => Class
148
     *
149
     * User models are variables with a . that should match an existing DataObject name
150
     *
151
     * @return array
152
     */
153
    public function getAvailableModels()
154
    {
155
        $fields = ['Content', 'Callout'];
156
157
        $models = self::config()->get('default_models');
158
159
        // Build a list of non namespaced models
160
        // They are not likely to clash anyway because of their unique table name
161
        $dataobjects = ClassInfo::getValidSubClasses(DataObject::class);
162
        $map = [];
163
        foreach ($dataobjects as $k => $v) {
164
            $parts = explode('\\', $v);
165
            $name = end($parts);
166
            $map[$name] = $v;
167
        }
168
169
        foreach ($fields as $field) {
170
            // Match variables with a dot in the call, like $MyModel.SomeMethod
171
            preg_match_all('/\$([a-zA-Z]+)\./m', $this->$field ?? '', $matches);
172
173
            if (!empty($matches) && !empty($matches[1])) {
174
                // Get unique model names
175
                $arr = array_unique($matches[1]);
176
177
                foreach ($arr as $name) {
178
                    if (!isset($map[$name])) {
179
                        continue;
180
                    }
181
                    $class = $map[$name];
182
                    $singl = singleton($class);
183
                    if ($singl instanceof DataObject) {
184
                        $models[$name] = $class;
185
                    }
186
                }
187
            }
188
        }
189
190
        return $models;
191
    }
192
193
    /**
194
     * Get an email template by code
195
     *
196
     * @param string $code
197
     * @param bool $alwaysReturn
198
     * @param string $locale
199
     * @return EmailTemplate
200
     */
201
    public static function getByCode($code, $alwaysReturn = true, $locale = null): ?static
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STATIC on line 201 at column 84
Loading history...
202
    {
203
        if ($locale) {
204
            $template = FluentHelper::withLocale($locale, function () use ($code) {
205
                return EmailTemplate::get()->filter('Code', $code)->first();
206
            });
207
        } else {
208
            $template = EmailTemplate::get()->filter('Code', $code)->first();
209
        }
210
        // Always return a template
211
        if (!$template && $alwaysReturn) {
212
            $template = new EmailTemplate();
213
            $template->Subject = $code;
214
            $template->Code = $code;
215
            $template->Content = 'Replace this with your own content and untick disabled';
216
            $template->Disabled = true;
217
            $template->write();
218
        }
219
        return $template;
220
    }
221
222
    /**
223
     * A shorthand to get an email by code
224
     *
225
     * @param string $code
226
     * @param string $locale
227
     * @return BetterEmail
228
     */
229
    public static function getEmailByCode($code, $locale = null)
230
    {
231
        return self::getByCode($code, true, $locale)->getEmail();
232
    }
233
234
    public function onBeforeWrite()
235
    {
236
        parent::onBeforeWrite();
237
    }
238
239
    /**
240
     * Content of the literal field for the merge fields
241
     *
242
     * @return string
243
     */
244
    protected function mergeFieldsHelper()
245
    {
246
        $content = '<strong>Base fields:</strong><br/>';
247
        $baseFields = [
248
            'To', 'Cc', 'Bcc', 'From', 'Subject', 'Body', 'BaseURL', 'Controller'
249
        ];
250
        foreach ($baseFields as $baseField) {
251
            $content .= $baseField . ', ';
252
        }
253
        $content = trim($content, ', ') . '<br/>';
254
255
        $models = $this->getAvailableModels();
256
257
        $modelsByClass = [];
258
        $classes = [];
259
        foreach ($models as $name => $model) {
260
            $classes[] = $model;
261
            if (!isset($modelsByClass[$model])) {
262
                $modelsByClass[$model] = [];
263
            }
264
            $modelsByClass[$model][] = $name;
265
        }
266
        $classes = array_unique($classes);
267
268
        $locales = [];
269
        // if (class_exists('Fluent')) {
270
        //     $locales = Fluent::locales();
271
        // }
272
273
        foreach ($classes as $model) {
274
            if (!class_exists($model)) {
275
                continue;
276
            }
277
            $props = Config::inst()->get($model, 'db');
278
            $o = singleton($model);
279
            $content .= '<strong>' . $model . ' (' . implode(',', $modelsByClass[$model]) . '):</strong><br/>';
280
            foreach ($props as $fieldName => $fieldType) {
281
                // Filter out locale fields
282
                foreach ($locales as $locale) {
283
                    if (strpos($fieldName, $locale) !== false) {
284
                        continue;
285
                    }
286
                }
287
                $content .= $fieldName . ', ';
288
            }
289
290
            // We could also show methods but that may be long
291
            if (self::config()->get('helper_show_methods')) {
292
                $methods = array_diff($o->allMethodNames(true), $o->allMethodNames());
293
                foreach ($methods as $method) {
294
                    if (strpos($method, 'get') === 0) {
295
                        $content .= $method . ', ';
296
                    }
297
                }
298
            }
299
300
            $content = trim($content, ', ') . '<br/>';
301
        }
302
        $content .= "<br/><div class='message info'>" . _t('EmailTemplate.ENCLOSEFIELD', 'To escape a field from surrounding text, you can enclose it between brackets, eg: {$Member.FirstName}.') . '</div>';
303
        return $content;
304
    }
305
306
    /**
307
     * Provide content for the Preview tab
308
     *
309
     * @return Tab
310
     */
311
    protected function previewTab()
312
    {
313
        $tab = new Tab('Preview');
314
315
        // Preview iframe
316
        $sanitisedModel =  str_replace('\\', '-', EmailTemplate::class);
317
        $adminSegment = EmailTemplatesAdmin::config()->get('url_segment');
318
        $adminBaseSegment = AdminRootController::config()->get('url_base');
319
        $iframeSrc = Director::baseURL() . $adminBaseSegment . '/' . $adminSegment . '/' . $sanitisedModel . '/PreviewEmail/?id=' . $this->ID;
320
        $iframe = new LiteralField('iframe', '<iframe src="' . $iframeSrc . '" style="width:800px;background:#fff;border:1px solid #ccc;min-height:500px;vertical-align:top"></iframe>');
321
        $tab->push($iframe);
322
323
        $env = Environment::getEnv('SS_SEND_ALL_EMAILS_TO');
324
        if ($env || Director::isDev()) {
325
            $sendTestLink = Director::baseURL() . $adminBaseSegment . '/' . $adminSegment . '/' . $sanitisedModel . '/SendTestEmailTemplate/?id=' . $this->ID . '&to=' . urlencode($env);
326
            $sendTest = new LiteralField("send_test", "<hr/><a href='$sendTestLink'>Send test email</a>");
327
            $tab->push($sendTest);
328
        }
329
330
        return $tab;
331
    }
332
333
    /**
334
     * Returns an instance of an Email with the content of the template
335
     *
336
     * @return BetterEmail
337
     */
338
    public function getEmail()
339
    {
340
        $email = Email::create();
341
        if (!$email instanceof BetterEmail) {
342
            throw new Exception("Make sure you are injecting the BetterEmail class instead of your base Email class");
343
        }
344
345
        $this->applyTemplate($email);
346
        if ($this->Disabled) {
347
            $email->setDisabled(true);
348
        }
349
        return $email;
350
    }
351
352
    /**
353
     * Returns an instance of an Email with the content tailored to the member
354
     *
355
     * @param Member $member
356
     * @return BetterEmail
357
     */
358
    public function getEmailForMember(Member $member)
359
    {
360
        $restoreLocale = null;
361
        if ($member->Locale) {
362
            $restoreLocale = i18n::get_locale();
363
            i18n::set_locale($member->Locale);
364
        }
365
366
        $email = $this->getEmail();
367
        $email->setToMember($member);
368
369
        if ($restoreLocale) {
370
            i18n::set_locale($restoreLocale);
371
        }
372
373
        return $email;
374
    }
375
376
    /**
377
     * Apply this template to the email
378
     *
379
     * @param BetterEmail $email
380
     */
381
    public function applyTemplate(&$email)
382
    {
383
        $email->setEmailTemplate($this);
384
385
        if ($this->Subject) {
386
            $email->setSubject($this->Subject);
387
        }
388
389
        // Use dbObject to handle shortcodes as well
390
        $email->setData([
391
            'EmailContent' => $this->dbObject('Content')->forTemplate(),
392
            'Callout' => $this->dbObject('Callout')->forTemplate(),
393
        ]);
394
395
        // Email are initialized with admin_email if set, we may want to use our own sender
396
        if ($this->DefaultSender) {
397
            $email->setFrom($this->DefaultSender);
398
        } else {
399
            $SiteConfig = SiteConfig::current_site_config();
400
            $email->setFrom($SiteConfig->EmailDefaultSender());
401
        }
402
        if ($this->DefaultRecipient) {
403
            $email->setTo($this->DefaultRecipient);
404
        }
405
406
        $this->extend('updateApplyTemplate', $email);
407
    }
408
409
    /**
410
     * Get rendered body
411
     *
412
     * @param bool $injectFake
413
     * @return string
414
     */
415
    public function renderTemplate($injectFake = false)
416
    {
417
        // Disable debug bar in the iframe
418
        Config::modify()->set('LeKoala\\DebugBar\\DebugBar', 'auto_inject', false);
419
420
        $email = $this->getEmail();
421
        if ($injectFake) {
422
            $email = $this->setPreviewData($email);
423
        }
424
425
        $html = $email->getRenderedBody();
426
427
        return $html;
428
    }
429
430
    /**
431
     * Inject random data into email for nicer preview
432
     *
433
     * @param BetterEmail $email
434
     * @return BetterEmail
435
     */
436
    public function setPreviewData(BetterEmail $email)
437
    {
438
        $data = [];
439
440
        // Get an array of data like ["Body" => "My content", "Callout" => "The callout..."]
441
        $emailData = $email->getData();
442
443
        // Parse the data for variables
444
        // For now, simply replace them by their name in curly braces
445
        foreach ($emailData as $k => $v) {
446
            if (!$v) {
447
                continue;
448
            }
449
450
            $matches = null;
451
452
            // This match all $Variable or $Member.Firstname kind of vars
453
            preg_match_all('/\$([a-zA-Z.]*)/', $v, $matches);
454
            if ($matches && !empty($matches[1])) {
455
                foreach ($matches[1] as $name) {
456
                    $name = trim($name, '.');
457
458
                    if (strpos($name, '.') !== false) {
459
                        // It's an object
460
                        $parts = explode('.', $name);
461
                        $objectName = array_shift($parts);
462
                        if (isset($data[$objectName])) {
463
                            $object = $data[$objectName];
464
                        } else {
465
                            $object = new ArrayData([]);
466
                        }
467
                        $curr = $object;
468
469
                        // May be recursive
470
                        foreach ($parts as $part) {
471
                            if (is_string($curr)) {
472
                                $curr = [];
473
                                $object->$part = $curr;
474
                            }
475
                            $object->$part = '{' . "$objectName.$part" . '}';
476
                            $prevPart = $part;
477
                            $curr = $object->$part;
478
                        }
479
                        $data[$objectName] = $object;
480
                    } else {
481
                        // It's a simple var
482
                        $data[$name] = '{' . $name . '}';
483
                    }
484
                }
485
            }
486
        }
487
488
        // Inject random data for known classes
489
        foreach ($this->getAvailableModels() as $name => $class) {
490
            if (!class_exists($class)) {
491
                continue;
492
            }
493
            if (singleton($class)->hasMethod('getSampleRecord')) {
494
                $o = $class::getSampleRecord();
495
            } else {
496
                $o = $class::get()->sort('RAND()')->first();
497
            }
498
499
            if (!$o) {
500
                $o = new $class;
501
            }
502
            $data[$name] = $o;
503
        }
504
505
        foreach ($data as $name => $value) {
506
            $email->addData($name, $value);
507
        }
508
509
        return $email;
510
    }
511
}
512