EmailImportTask::run()   F
last analyzed

Complexity

Conditions 54
Paths > 20000

Size

Total Lines 278
Code Lines 159

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 54
eloc 159
c 1
b 1
f 0
nc 26123136
nop 1
dl 0
loc 278
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\EmailTemplates\Tasks;
4
5
use Exception;
6
use DOMElement;
7
use DOMDocument;
8
use SilverStripe\ORM\DB;
9
use SilverStripe\i18n\i18n;
10
use SilverStripe\Dev\BuildTask;
11
use SilverStripe\Control\Director;
12
use TractorCow\Fluent\Model\Locale;
0 ignored issues
show
Bug introduced by
The type TractorCow\Fluent\Model\Locale was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use SilverStripe\Core\Manifest\ModuleLoader;
14
use LeKoala\EmailTemplates\Helpers\FluentHelper;
15
use LeKoala\EmailTemplates\Models\EmailTemplate;
16
use TractorCow\Fluent\Extension\FluentExtension;
0 ignored issues
show
Bug introduced by
The type TractorCow\Fluent\Extension\FluentExtension was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use LeKoala\EmailTemplates\Helpers\SubsiteHelper;
18
use SilverStripe\Core\Config\Config;
19
use SilverStripe\i18n\TextCollection\i18nTextCollector;
20
21
/**
22
 * Import email templates provided from ss files
23
 *
24
 * \vendor\silverstripe\framework\templates\SilverStripe\Control\Email
25
 * \vendor\silverstripe\framework\templates\SilverStripe\Control\Email\ForgotPasswordEmail.ss
26
 *
27
 * \app\templates\Email\MySampleEmail.ss
28
 *
29
 * Finds all *Email.ss templates and imports them into the CMS
30
 * @author lekoala
31
 */
32
class EmailImportTask extends BuildTask
33
{
34
    private static $segment = 'EmailImportTask';
35
36
    protected $title = "Email import task";
37
    protected $description = "Finds all *Email.ss templates and imports them into the CMS, if they don't already exist.";
38
39
    public function run($request)
40
    {
41
        $subsiteSupport = SubsiteHelper::usesSubsite();
42
        $fluentSupport = FluentHelper::usesFluent();
43
44
        echo 'Run with ?clear=1 to clear empty database before running the task<br/>';
45
        echo 'Run with ?overwrite=soft|hard to overwrite templates that exists in the cms. Soft will replace template if not modified by the user, hard will replace template even if modified by user.<br/>';
46
        echo 'Run with ?templates=xxx,yyy to specify which template should be imported<br/>';
47
        if ($subsiteSupport) {
48
            echo 'Run with ?subsite=all|subsiteID to create email templates in all subsites (including main site) or only in the chosen subsite (if a subsite is active, it will be used by default).<br/>';
49
        }
50
        if ($fluentSupport) {
51
            echo 'Run with ?locales=fr,en to choose which locale to import.<br/>';
52
        }
53
        echo '<strong>Remember to flush the templates/translations if needed</strong><br/>';
54
        echo '<hr/>';
55
56
        $overwrite = $request->getVar('overwrite');
57
        $clear = $request->getVar('clear');
58
        $templatesToImport = $request->getVar('templates');
59
        $importToSubsite = $request->getVar('subsite');
0 ignored issues
show
Unused Code introduced by
The assignment to $importToSubsite is dead and can be removed.
Loading history...
60
        $chosenLocales = $request->getVar('locales');
61
62
        // Normalize argument
63
        if ($overwrite && $overwrite != 'soft' && $overwrite != 'hard') {
64
            $overwrite = 'soft';
65
        }
66
67
        // Select which subsite to import emails to
68
        $importToSubsite = array();
69
        if ($subsiteSupport) {
70
            $subsites = array();
71
            if ($importToSubsite == 'all') {
0 ignored issues
show
introduced by
The condition $importToSubsite == 'all' is always false.
Loading history...
72
                $subsites = SubsiteHelper::listSubsites();
73
            } elseif (is_numeric($importToSubsite)) {
0 ignored issues
show
introduced by
The condition is_numeric($importToSubsite) is always false.
Loading history...
74
                $subsites = SubsiteHelper::listSubsites();
75
                $subsiteTitle = 'Subsite #' . $importToSubsite;
76
                foreach ($subsites as $subsite) {
77
                    if ($subsite->ID == $importToSubsite) {
78
                        $subsiteTitle = $subsite->Title;
79
                    }
80
                }
81
                $subsites = array(
82
                    $importToSubsite => $subsiteTitle
83
                );
84
            }
85
            if ($subsiteSupport && SubsiteHelper::currentSubsiteID()) {
86
                DB::alteration_message("Importing to current subsite. Run from main site to import other subsites at once.", "created");
87
                $subsites = array();
88
            }
89
            if (!empty($subsites)) {
90
                DB::alteration_message("Importing to subsites : " . implode(',', array_values($subsites)), "created");
91
            }
92
        }
93
94
        if ($templatesToImport) {
95
            $templatesToImport = explode(',', $templatesToImport);
96
        }
97
98
        // Do we clear our templates?
99
        if ($clear == 1) {
100
            DB::alteration_message("Clear all email templates", "created");
101
            $emailTemplates = EmailTemplate::get();
102
            foreach ($emailTemplates as $emailTemplate) {
103
                $emailTemplate->delete();
104
            }
105
        }
106
107
        $emailTemplateSingl = singleton(EmailTemplate::class);
0 ignored issues
show
Unused Code introduced by
The assignment to $emailTemplateSingl is dead and can be removed.
Loading history...
108
109
        $locales = null;
110
        if ($fluentSupport) {
111
            if (FluentHelper::isClassTranslated(EmailTemplate::class)) {
112
                $locales = Locale::get()->column('Locale');
113
114
                // We collect only one locale, restrict the list
115
                if ($chosenLocales) {
116
                    $arr = explode(',', $chosenLocales);
117
                    $locales = array();
118
                    foreach ($arr as $a) {
119
                        $a = FluentHelper::get_locale_from_lang($a);
120
                        $locales[] = $a;
121
                    }
122
                }
123
            }
124
        }
125
126
        $defaultLocale = FluentHelper::get_locale();
127
128
        $templates = $this->collectTemplates();
129
130
        // don't throw errors
131
        Config::modify()->set(i18n::class, 'missing_default_warning', false);
132
133
        foreach ($templates as $filePath) {
134
            $isOverwritten = false;
135
136
            $fileName = basename($filePath, '.ss');
137
138
            // Remove base path
139
            $relativeFilePath = str_replace(Director::baseFolder(), '', $filePath);
140
            $relativeFilePathParts = explode('/', trim($relativeFilePath, '/'));
141
142
            // Group by module
143
            $moduleName = array_shift($relativeFilePathParts);
144
            if ($moduleName == 'vendor') {
145
                $moduleVendor = array_shift($relativeFilePathParts);
146
                // get module name
147
                $moduleName = $moduleVendor . '/' . array_shift($relativeFilePathParts);
148
            }
149
150
            // remove /templates part
151
            array_shift($relativeFilePathParts);
152
            $templateName = str_replace('.ss', '', implode('/', $relativeFilePathParts));
153
154
            $templateTitle = basename($templateName);
0 ignored issues
show
Unused Code introduced by
The assignment to $templateTitle is dead and can be removed.
Loading history...
155
156
            // Create the email code (basically, the template name without "Email" at the end)
157
            $code = preg_replace('/Email$/', '', $fileName);
158
159
            if (!empty($templatesToImport) && !in_array($code, $templatesToImport)) {
160
                DB::alteration_message("Template with code <b>$code</b> was ignored.", "repaired");
161
                continue;
162
            }
163
164
            $whereCode = array(
165
                'Code' => $code
166
            );
167
            $emailTemplate = EmailTemplate::get()->filter($whereCode)->first();
168
169
            // Check if it has been modified or not
170
            $templateModified = false;
171
            if ($emailTemplate) {
172
                $templateModified = $emailTemplate->Created != $emailTemplate->LastEdited;
173
            }
174
175
            if (!$overwrite && $emailTemplate) {
176
                DB::alteration_message("Template with code <b>$code</b> already exists. Choose overwrite if you want to import again.", "repaired");
177
                continue;
178
            }
179
            if ($overwrite == 'soft' && $templateModified) {
180
                DB::alteration_message("Template with code <b>$code</b> has been modified by the user. Choose overwrite=hard to change.", "repaired");
181
                continue;
182
            }
183
184
            // Create a default title from code
185
            $title = preg_split('/(?=[A-Z])/', $code);
186
            $title = implode(' ', $title);
0 ignored issues
show
Unused Code introduced by
The assignment to $title is dead and can be removed.
Loading history...
187
188
            // Get content of the email
189
            $content = file_get_contents($filePath);
190
191
            // Analyze content to find incompatibilities
192
            $errors = self::checkContentForErrors($content);
193
            if (!empty($errors)) {
194
                echo "<div style='color:red'>Invalid syntax was found in '$relativeFilePath'. Please fix these errors before importing the template<ul>";
195
                foreach ($errors as $error) {
196
                    echo '<li>' . $error . '</li>';
197
                }
198
                echo '</ul></div>';
199
                continue;
200
            }
201
202
            // Parse language
203
            $module = ModuleLoader::getModule($moduleName);
204
            $collector = new i18nTextCollector;
205
            $entities = $collector->collectFromTemplate($content, $fileName, $module);
206
207
            /*
208
            array:1 [▼
209
            "MyEmail.SUBJECT" => "My subject"
210
            ]
211
            */
212
213
            $translationTable = array();
214
            foreach ($entities as $entity => $data) {
215
                if ($locales) {
216
                    foreach ($locales as $locale) {
217
                        i18n::set_locale($locale);
218
                        if (!isset($translationTable[$entity])) {
219
                            $translationTable[$entity] = array();
220
                        }
221
                        $translationTable[$entity][$locale] = i18n::_t($entity, $data);
222
                    }
223
                    i18n::set_locale($defaultLocale);
224
                } else {
225
                    $translationTable[$entity] = array($defaultLocale => i18n::_t($entity, $data));
226
                }
227
            }
228
229
            $contentLocale = array();
230
            // May be null
231
            if ($locales) {
232
                foreach ($locales as $locale) {
233
                    $contentLocale[$locale] = $content;
234
                }
235
            }
236
            if (!isset($contentLocale[$defaultLocale])) {
237
                $contentLocale[$defaultLocale] = $content;
238
            }
239
240
            // Now we use our translation table to manually replace _t calls into file content
241
            foreach ($translationTable as $entity => $translationData) {
242
                // use the double escape notation
243
                $escapedEntity = str_replace('\\', '\\\\\\\\', $entity);
244
                // fix dot notation
245
                $escapedEntity = str_replace('.', '\.', $escapedEntity);
246
                $baseTranslation = null;
247
248
                foreach ($translationData as $locale => $translation) {
249
                    if (!$baseTranslation && $translation) {
250
                        $baseTranslation = $translation;
251
                    }
252
                    if (!$translation) {
253
                        $translation = $baseTranslation;
254
                    }
255
                    // This regex should match old and new style
256
                    $count = 0;
257
                    $contentLocale[$locale] = preg_replace("/<%(t | _t\(')" . $escapedEntity . "( |').*?%>/ums", $translation, $contentLocale[$locale], -1, $count);
258
                    if (!$count) {
259
                        throw new Exception("Failed to replace $escapedEntity with translation $translation");
260
                    }
261
                }
262
            }
263
264
            // Create a template if necassery or mark as overwritten
265
            if (!$emailTemplate) {
266
                $emailTemplate = new EmailTemplate;
267
            } else {
268
                $isOverwritten = true;
269
            }
270
271
            // Other properties
272
            $emailTemplate->Code = $code;
273
            $emailTemplate->Category = $moduleName;
274
            if (SubsiteHelper::currentSubsiteID() && !$emailTemplate->SubsiteID) {
0 ignored issues
show
Bug Best Practice introduced by
The property SubsiteID does not exist on LeKoala\EmailTemplates\Models\EmailTemplate. Since you implemented __get, consider adding a @property annotation.
Loading history...
275
                $emailTemplate->SubsiteID = SubsiteHelper::currentSubsiteID();
0 ignored issues
show
Bug Best Practice introduced by
The property SubsiteID does not exist on LeKoala\EmailTemplates\Models\EmailTemplate. Since you implemented __set, consider adding a @property annotation.
Loading history...
276
            }
277
            // Write to main site or current subsite
278
            $emailTemplate->write();
279
280
            // Apply content to email after write to ensure we can localize properly
281
            $this->assignContent($emailTemplate, $contentLocale[$defaultLocale]);
282
283
            if (!empty($locales)) {
284
                foreach ($locales as $locale) {
285
                    $this->assignContent($emailTemplate, $contentLocale[$locale], $locale);
286
                }
287
            }
288
289
            // Reset date to allow tracking user edition (for soft/hard overwrite)
290
            $this->resetLastEditedDate($emailTemplate->ID);
291
292
            // Loop through subsites
293
            if (!empty($importToSubsite)) {
294
                SubsiteHelper::disableFilter();
295
                foreach ($subsites as $subsiteID => $subsiteTitle) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $subsites does not seem to be defined for all execution paths leading up to this point.
Loading history...
296
                    $whereCode['SubsiteID'] = $subsiteID;
297
298
                    $subsiteEmailTemplate = EmailTemplate::get()->filter($whereCode)->first();
299
300
                    $emailTemplateCopy = $emailTemplate;
301
                    $emailTemplateCopy->SubsiteID = $subsiteID;
302
                    if ($subsiteEmailTemplate) {
303
                        $emailTemplateCopy->ID = $subsiteEmailTemplate->ID;
304
                    } else {
305
                        $emailTemplateCopy->ID = 0; // New
306
                    }
307
                    $emailTemplateCopy->write();
308
309
                    $this->resetLastEditedDate($emailTemplateCopy->ID);
310
                }
311
            }
312
313
            if ($isOverwritten) {
314
                DB::alteration_message("Overwrote <b>{$emailTemplate->Code}</b>", "created");
315
            } else {
316
                DB::alteration_message("Imported <b>{$emailTemplate->Code}</b>", "created");
317
            }
318
        }
319
    }
320
321
    public static function checkContentForErrors($content)
322
    {
323
        $errors = array();
324
        if (strpos($content, '<% with') !== false) {
325
            $errors[] = 'Replace "with" blocks by plain calls to the variable';
326
        }
327
        if (strpos($content, '<% if') !== false) {
328
            $errors[] = 'If/else logic is not supported. Please create one template by use case or abstract logic into the model';
329
        }
330
        if (strpos($content, '<% loop') !== false) {
331
            $errors[] = 'Loops are not supported. Please create a helper method on the model to render the loop';
332
        }
333
        if (strpos($content, '<% sprintf') !== false) {
334
            $errors[] = 'You should not use sprintf to escape content, please use plain _t calls';
335
        }
336
        return $errors;
337
    }
338
339
    /**
340
     * Collect email from your project
341
     *
342
     * @return array
343
     */
344
    protected function collectTemplates()
345
    {
346
        $templates = glob(Director::baseFolder() . '/' . project() . '/templates/Email/*Email.ss');
347
348
        $framework = self::config()->import_framework;
349
        if ($framework) {
350
            // use ? to avoid matching plain Email.ss
351
            $templates = array_merge($templates, glob(Director::baseFolder() . '/vendor/silverstripe/framework/templates/SilverStripe/Control/Email/?*Email.ss'));
352
        }
353
        $extra = self::config()->extra_paths;
354
        foreach ($extra as $path) {
355
            $path = trim($path, '/');
356
            $templates = array_merge($templates, glob(Director::baseFolder() . '/' . $path . '/*Email.ss'));
357
        }
358
359
        return $templates;
360
    }
361
362
    /**
363
     * Utility function to reset email templates last edited date
364
     *
365
     * @param int $ID
366
     * @return void
367
     */
368
    protected function resetLastEditedDate($ID)
369
    {
370
        DB::query("UPDATE `EmailTemplate` SET LastEdited = Created WHERE ID = " . $ID);
371
    }
372
373
    /**
374
     * Update a template with content
375
     *
376
     * @param EmailTemplate $emailTemplate
377
     * @param string $content The full page content with html
378
     * @param string $locale
379
     * @return void
380
     */
381
    protected function assignContent(EmailTemplate $emailTemplate, $content, $locale = '')
382
    {
383
        FluentHelper::withLocale($locale, function () use ($emailTemplate, $content) {
384
            // First assign the whole string to Content in case it's not split by zones
385
            $cleanContent = $this->cleanContent($content);
386
            $emailTemplate->Content = '';
387
            $emailTemplate->Content = $cleanContent;
388
389
            $source = '<!DOCTYPE html><html><body>' . $content . '</body></html>';
390
            $dom = new DomDocument('1.0', 'UTF-8');
391
            $dom->loadHTML(mb_convert_encoding($source, 'HTML-ENTITIES', 'UTF-8'));
0 ignored issues
show
Bug introduced by
It seems like mb_convert_encoding($sou...TML-ENTITIES', 'UTF-8') can also be of type array; however, parameter $source of DOMDocument::loadHTML() 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 ignore-type  annotation

391
            $dom->loadHTML(/** @scrutinizer ignore-type */ mb_convert_encoding($source, 'HTML-ENTITIES', 'UTF-8'));
Loading history...
392
393
            // Look for nodes to assign to proper fields (will overwrite content)
394
            $fields = array('Content', 'Callout', 'Subject');
395
            foreach ($fields as $field) {
396
                $node = $dom->getElementById($field);
397
                if ($node) {
398
                    $cleanContent = $this->cleanContent($this->getInnerHtml($node));
399
                    $emailTemplate->$field = '';
400
                    $emailTemplate->$field = $cleanContent;
401
                }
402
            }
403
404
            // No subject? try conventions
405
            if (!$emailTemplate->Subject) {
406
                switch ($emailTemplate->Code) {
407
                    case 'ChangePassword':
408
                        $entity = "SilverStripe\\Security\\Member.SUBJECTPASSWORDCHANGED";
409
                        break;
410
                    case 'ForgotPassword':
411
                        $entity = "SilverStripe\\Security\\Member.SUBJECTPASSWORDRESET";
412
                        break;
413
                    default:
414
                        $entity = $emailTemplate->Code . 'Email.SUBJECT';
415
                        break;
416
                }
417
                $subject = _t($entity);
418
                if ($subject) {
419
                    $emailTemplate->Subject = $subject;
420
                }
421
            }
422
423
            // Write each time within the given state
424
            $emailTemplate->write();
425
        });
426
    }
427
428
    /**
429
     * Get a clean string
430
     *
431
     * @param string $content
432
     * @return string
433
     */
434
    protected function cleanContent($content)
435
    {
436
        $content = strip_tags($content, '<p><br><br/><div><img><a><span><ul><li><strong><em><b><i><blockquote><h1><h2><h3><h4><h5><h6>');
437
        $content = str_replace("’", "'", $content);
438
        $content = trim($content);
439
        $content = nl2br($content);
440
        return $content;
441
    }
442
443
    /**
444
     * Loop over a node to extract all html
445
     *
446
     * @param DOMElement $node
447
     * @return string
448
     */
449
    protected function getInnerHtml(DOMElement $node)
450
    {
451
        $innerHTML = '';
452
        $children = $node->childNodes;
453
        foreach ($children as $child) {
454
            $innerHTML .= $child->ownerDocument->saveXML($child);
455
        }
456
        return $innerHTML;
457
    }
458
}
459