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
|
|||
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 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.