Passed
Push — master ( 2ea0b2...d8ed7e )
by Thomas
02:39 queued 11s
created

src/Models/EmailTemplate.php (1 issue)

Severity
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\Security\Member;
12
use SilverStripe\Forms\HeaderField;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Forms\LiteralField;
15
use SilverStripe\Control\Email\Email;
16
use SilverStripe\Forms\DropdownField;
17
use SilverStripe\Security\Permission;
18
use LeKoala\EmailTemplates\Email\BetterEmail;
19
use LeKoala\EmailTemplates\Admin\EmailTemplatesAdmin;
20
use SilverStripe\Forms\CheckboxField;
21
use SilverStripe\Forms\TextField;
22
use SilverStripe\SiteConfig\SiteConfig;
23
24
/**
25
 * User defined email templates
26
 *
27
 * Content of the template should override default content provided with setHTMLTemplate
28
 *
29
 * For example, in the framework we have
30
 *    $email = Email::create()->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
31
 *
32
 * It means our template code should match this : ForgotPasswordEmail
33
 *
34
 * @property string $Subject
35
 * @property string $DefaultSender
36
 * @property string $DefaultRecipient
37
 * @property string $Category
38
 * @property string $Code
39
 * @property string $Content
40
 * @property string $Callout
41
 * @property boolean $Disabled
42
 * @author lekoala
43
 */
44
class EmailTemplate extends DataObject
45
{
46
    private static $table_name = 'EmailTemplate';
47
48
    private static $db = array(
49
        'Subject' => 'Varchar(255)',
50
        'DefaultSender' => 'Varchar(255)',
51
        'DefaultRecipient' => 'Varchar(255)',
52
        'Category' => 'Varchar(255)',
53
        'Code' => 'Varchar(255)',
54
        // Content
55
        'Content' => 'HTMLText',
56
        'Callout' => 'HTMLText',
57
        // Configuration
58
        'Disabled' => 'Boolean',
59
    );
60
    private static $summary_fields = array(
61
        'Subject',
62
        'Code',
63
        'Category',
64
        'Disabled',
65
    );
66
    private static $searchable_fields = array(
67
        'Subject',
68
        'Code',
69
        'Category',
70
        'Disabled',
71
    );
72
    private static $indexes = array(
73
        'Code' => true, // Code is not unique because it can be used by subsites
74
    );
75
    private static $translate = array(
76
        'Subject', 'Content', 'Callout'
77
    );
78
79
    public function getTitle()
80
    {
81
        return $this->Subject;
82
    }
83
84
    public function getCMSFields()
85
    {
86
        $fields = parent::getCMSFields();
87
88
        // Do not allow changing subsite
89
        $fields->removeByName('SubsiteID');
90
91
        $fields->dataFieldByName('Callout')->setRows(5);
92
93
        $codeField = $fields->dataFieldByName('Code');
94
        $codeField->setAttribute('placeholder', _t('EmailTemplate.CODEPLACEHOLDER', 'A unique code that will be used in code to retrieve the template, e.g.: MyEmail'));
95
96
        if ($this->Code) {
97
            $codeField->setReadonly(true);
98
        }
99
100
        // Merge fields helper
101
        $fields->addFieldToTab('Root.Main', new HeaderField('MergeFieldsHelperTitle', _t('EmailTemplate.AVAILABLEMERGEFIELDSTITLE', 'Available merge fields')));
102
103
        $fields->addFieldToTab('Root.Main', new LiteralField('MergeFieldsHelper', $this->mergeFieldsHelper()));
104
105
        if ($this->ID) {
106
            $fields->addFieldToTab('Root.Preview', $this->previewTab());
107
        }
108
109
        // Cleanup UI
110
        $categories = EmailTemplate::get()->column('Category');
111
        $fields->addFieldsToTab('Root.Settings', new DropdownField('Category', 'Category', array_combine($categories, $categories)));
112
        $fields->addFieldsToTab('Root.Settings', new CheckboxField('Disabled'));
113
        $fields->addFieldsToTab('Root.Settings', new TextField('DefaultSender'));
114
        $fields->addFieldsToTab('Root.Settings', new TextField('DefaultRecipient'));
115
116
117
        return $fields;
118
    }
119
120
    public function canView($member = null)
121
    {
122
        return true;
123
    }
124
125
    public function canEdit($member = null)
126
    {
127
        return Permission::check('CMS_ACCESS', 'any', $member);
128
    }
129
130
    public function canCreate($member = null, $context = [])
131
    {
132
        return Permission::check('CMS_ACCESS', 'any', $member);
133
    }
134
135
    public function canDelete($member = null)
136
    {
137
        return Permission::check('CMS_ACCESS', 'any', $member);
138
    }
139
140
    /**
141
     * A map of Name => Class
142
     *
143
     * User models are variables with a . that should match an existing DataObject name
144
     *
145
     * @return array
146
     */
147
    public function getAvailableModels()
148
    {
149
        $fields = ['Content', 'Callout'];
150
151
        $models = self::config()->default_models;
152
153
        // Build a list of non namespaced models
154
        // They are not likely to clash anyway because of their unique table name
155
        $dataobjects = ClassInfo::getValidSubClasses(DataObject::class);
156
        $map = [];
157
        foreach ($dataobjects as $k => $v) {
158
            $parts = explode('\\', $v);
159
            $name = end($parts);
160
            $map[$name] = $v;
161
        }
162
163
        foreach ($fields as $field) {
164
            // Match variables with a dot in the call, like $MyModel.SomeMethod
165
            preg_match_all('/\$([a-zA-Z]+)\./m', $this->$field, $matches);
166
167
            if (!empty($matches) && !empty($matches[1])) {
168
                // Get unique model names
169
                $arr = array_unique($matches[1]);
170
171
                foreach ($arr as $name) {
172
                    if (!isset($map[$name])) {
173
                        continue;
174
                    }
175
                    $class = $map[$name];
176
                    $singl = singleton($class);
177
                    if ($singl instanceof DataObject) {
178
                        $models[$name] = $class;
179
                    }
180
                }
181
            }
182
        }
183
184
        return $models;
185
    }
186
187
    /**
188
     * Get an email template by code
189
     *
190
     * @param string $code
191
     * @param bool $alwaysReturn
192
     * @return EmailTemplate
193
     */
194
    public static function getByCode($code, $alwaysReturn = true)
195
    {
196
        $template = EmailTemplate::get()->filter('Code', $code)->first();
197
        // Always return a template
198
        if (!$template && $alwaysReturn) {
199
            $template = new EmailTemplate();
200
            $template->Subject = $code;
201
            $template->Code = $code;
202
            $template->Content = 'Replace this with your own content and untick disabled';
203
            $template->Disabled = true;
204
            $template->write();
205
        }
206
        return $template;
207
    }
208
209
    /**
210
     * A shorthand to get an email by code
211
     *
212
     * @param string $code
213
     * @return BetterEmail
214
     */
215
    public static function getEmailByCode($code)
216
    {
217
        return self::getByCode($code)->getEmail();
218
    }
219
220
    public function onBeforeWrite()
221
    {
222
        parent::onBeforeWrite();
223
    }
224
225
    /**
226
     * Content of the literal field for the merge fields
227
     *
228
     * @return string
229
     */
230
    protected function mergeFieldsHelper()
231
    {
232
        $content = '<strong>Base fields:</strong><br/>';
233
        $baseFields = array(
234
            'To', 'Cc', 'Bcc', 'From', 'Subject', 'Body', 'BaseURL', 'Controller'
235
        );
236
        foreach ($baseFields as $baseField) {
237
            $content .= $baseField . ', ';
238
        }
239
        $content = trim($content, ', ') . '<br/>';
240
241
        $models = $this->getAvailableModels();
242
243
        $modelsByClass = array();
244
        $classes = array();
245
        foreach ($models as $name => $model) {
246
            $classes[] = $model;
247
            if (!isset($modelsByClass[$model])) {
248
                $modelsByClass[$model] = array();
249
            }
250
            $modelsByClass[$model][] = $name;
251
        }
252
        $classes = array_unique($classes);
253
254
        $locales = array();
255
        // if (class_exists('Fluent')) {
256
        //     $locales = Fluent::locales();
257
        // }
258
259
        foreach ($classes as $model) {
260
            if (!class_exists($model)) {
261
                continue;
262
            }
263
            $props = Config::inst()->get($model, 'db');
264
            $o = singleton($model);
265
            $content .= '<strong>' . $model . ' (' . implode(',', $modelsByClass[$model]) . '):</strong><br/>';
266
            foreach ($props as $fieldName => $fieldType) {
267
                // Filter out locale fields
268
                foreach ($locales as $locale) {
269
                    if (strpos($fieldName, $locale) !== false) {
270
                        continue;
271
                    }
272
                }
273
                $content .= $fieldName . ', ';
274
            }
275
276
            // We could also show methods but that may be long
277
            if (self::config()->helper_show_methods) {
278
                $methods = array_diff($o->allMethodNames(true), $o->allMethodNames());
279
                foreach ($methods as $method) {
280
                    if (strpos($method, 'get') === 0) {
281
                        $content .= $method . ', ';
282
                    }
283
                }
284
            }
285
286
            $content = trim($content, ', ') . '<br/>';
287
        }
288
        $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>';
289
        return $content;
290
    }
291
292
    /**
293
     * Provide content for the Preview tab
294
     *
295
     * @return Tab
296
     */
297
    protected function previewTab()
298
    {
299
        $tab = new Tab('Preview');
300
301
        // Preview iframe
302
        $sanitisedModel =  str_replace('\\', '-', EmailTemplate::class);
303
        $adminSegment = EmailTemplatesAdmin::config()->url_segment;
304
        $iframeSrc = '/admin/' . $adminSegment . '/' . $sanitisedModel . '/PreviewEmail/?id=' . $this->ID;
305
        $iframe = new LiteralField('iframe', '<iframe src="' . $iframeSrc . '" style="width:800px;background:#fff;border:1px solid #ccc;min-height:500px;vertical-align:top"></iframe>');
306
        $tab->push($iframe);
307
308
        return $tab;
309
    }
310
311
    /**
312
     * Returns an instance of an Email with the content of the template
313
     *
314
     * @return BetterEmail
315
     */
316
    public function getEmail()
317
    {
318
        $email = Email::create();
319
        if (!$email instanceof BetterEmail) {
320
            throw new Exception("Make sure you are injecting the BetterEmail class instead of your base Email class");
321
        }
322
        $this->applyTemplate($email);
323
        if ($this->Disabled) {
324
            $email->setDisabled(true);
325
        }
326
        return $email;
327
    }
328
329
    /**
330
     * Returns an instance of an Email with the content tailored to the member
331
     *
332
     * @param Member $member
333
     * @return BetterEmail
334
     */
335
    public function getEmailForMember(Member $member)
336
    {
337
        $restoreLocale = null;
338
        if ($member->Locale) {
339
            $restoreLocale = i18n::get_locale();
340
            i18n::set_locale($member->Locale);
341
        }
342
343
        $email = $this->getEmail();
344
        $email->setToMember($member);
345
346
        if ($restoreLocale) {
347
            i18n::set_locale($restoreLocale);
348
        }
349
350
        return $email;
351
    }
352
353
    /**
354
     * Apply this template to the email
355
     *
356
     * @param BetterEmail $email
357
     */
358
    public function applyTemplate(&$email)
359
    {
360
        $email->setEmailTemplate($this);
361
362
        if ($this->Subject) {
363
            $email->setSubject($this->Subject);
364
        }
365
366
        // Use dbObject to handle shortcodes as well
367
        $email->setData([
368
            'EmailContent' => $this->dbObject('Content')->forTemplate(),
369
            'Callout' => $this->dbObject('Callout')->forTemplate(),
370
        ]);
371
372
        if ($this->DefaultSender) {
373
            $email->setFrom($this->DefaultSender);
374
        }
375
        if ($this->DefaultRecipient) {
376
            $email->setTo($this->DefaultRecipient);
377
        }
378
379
        $this->extend('updateApplyTemplate', $email);
380
    }
381
382
    /**
383
     * Get rendered body
384
     *
385
     * @param bool $parse Should we parse variables or not?
386
     * @param bool $injectFake
387
     * @return string
388
     */
389
    public function renderTemplate($parse = false, $injectFake = false)
0 ignored issues
show
The parameter $parse is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

389
    public function renderTemplate(/** @scrutinizer ignore-unused */ $parse = false, $injectFake = false)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
390
    {
391
        // Disable debug bar in the iframe
392
        Config::modify()->set('LeKoala\\DebugBar\\DebugBar', 'auto_inject', false);
393
394
        $email = $this->getEmail();
395
        if ($injectFake) {
396
            $email = $this->setPreviewData($email);
397
        }
398
399
        $html = $email->getRenderedBody();
400
401
        return $html;
402
    }
403
404
    /**
405
     * Inject random data into email for nicer preview
406
     *
407
     * @param BetterEmail $email
408
     * @return BetterEmail
409
     */
410
    public function setPreviewData(BetterEmail $email)
411
    {
412
        $data = array();
413
414
        // Get an array of data like ["Body" => "My content", "Callout" => "The callout..."]
415
        $emailData = $email->getData();
416
417
        // Parse the data for variables
418
        // For now, simply replace them by their name in curly braces
419
        foreach ($emailData as $k => $v) {
420
            if (!$v) {
421
                continue;
422
            }
423
424
            $matches = null;
425
426
            // This match all $Variable or $Member.Firstname kind of vars
427
            preg_match_all('/\$([a-zA-Z.]*)/', $v, $matches);
428
            if ($matches && !empty($matches[1])) {
429
                foreach ($matches[1] as $name) {
430
                    $name = trim($name, '.');
431
432
                    if (strpos($name, '.') !== false) {
433
                        // It's an object
434
                        $parts = explode('.', $name);
435
                        $objectName = array_shift($parts);
436
                        if (isset($data[$objectName])) {
437
                            $object = $data[$objectName];
438
                        } else {
439
                            $object = new ArrayData(array());
440
                        }
441
                        $curr = $object;
442
443
                        // May be recursive
444
                        foreach ($parts as $part) {
445
                            if (is_string($curr)) {
446
                                $curr = [];
447
                                $object->$part = $curr;
448
                            }
449
                            $object->$part = '{' . "$objectName.$part" . '}';
450
                            $prevPart = $part;
451
                            $curr = $object->$part;
452
                        }
453
                        $data[$objectName] = $object;
454
                    } else {
455
                        // It's a simple var
456
                        $data[$name] = '{' . $name . '}';
457
                    }
458
                }
459
            }
460
        }
461
462
        // Inject random data for known classes
463
        foreach ($this->getAvailableModels() as $name => $class) {
464
            if (!class_exists($class)) {
465
                continue;
466
            }
467
            if (singleton($class)->hasMethod('getSampleRecord')) {
468
                $o = $class::getSampleRecord();
469
            } else {
470
                $o = $class::get()->sort('RAND()')->first();
471
            }
472
473
            if (!$o) {
474
                $o = new $class;
475
            }
476
            $data[$name] = $o;
477
        }
478
479
        foreach ($data as $name => $value) {
480
            $email->addData($name, $value);
481
        }
482
483
        return $email;
484
    }
485
}
486