Issues (3627)

app/bundles/CoreBundle/Helper/ThemeHelper.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\CoreBundle\Helper;
13
14
use Mautic\CoreBundle\Exception as MauticException;
15
use Mautic\CoreBundle\Templating\Helper\ThemeHelper as TemplatingThemeHelper;
16
use Symfony\Component\Filesystem\Filesystem;
17
use Symfony\Component\Finder\Finder;
18
use Symfony\Component\Templating\EngineInterface;
19
use Symfony\Component\Templating\TemplateReference;
20
use Symfony\Component\Translation\TranslatorInterface;
21
22
class ThemeHelper
23
{
24
    /**
25
     * @var PathsHelper
26
     */
27
    private $pathsHelper;
28
29
    /**
30
     * @var TemplatingHelper
31
     */
32
    private $templatingHelper;
33
34
    /**
35
     * @var TranslatorInterface
36
     */
37
    private $translator;
38
39
    /**
40
     * @var array|mixed
41
     */
42
    private $themes = [];
43
44
    /**
45
     * @var array
46
     */
47
    private $themesInfo = [];
48
49
    /**
50
     * @var array
51
     */
52
    private $steps = [];
53
54
    /**
55
     * @var string
56
     */
57
    private $defaultTheme;
58
59
    /**
60
     * @var TemplatingThemeHelper[]
61
     */
62
    private $themeHelpers = [];
63
64
    /**
65
     * Default themes which cannot be deleted.
66
     *
67
     * @var array
68
     */
69
    protected $defaultThemes = [
70
        'aurora',
71
        'blank',
72
        'cards',
73
        'fresh-center',
74
        'fresh-fixed',
75
        'fresh-left',
76
        'fresh-wide',
77
        'goldstar',
78
        'neopolitan',
79
        'oxygen',
80
        'skyline',
81
        'sparse',
82
        'sunday',
83
        'system',
84
        'vibrant',
85
    ];
86
87
    /**
88
     * @var CoreParametersHelper
89
     */
90
    private $coreParametersHelper;
91
92
    /**
93
     * ThemeHelper constructor.
94
     */
95
    public function __construct(PathsHelper $pathsHelper, TemplatingHelper $templatingHelper, TranslatorInterface $translator, CoreParametersHelper $coreParametersHelper)
96
    {
97
        $this->pathsHelper          = $pathsHelper;
98
        $this->templatingHelper     = $templatingHelper;
99
        $this->translator           = $translator;
100
        $this->coreParametersHelper = $coreParametersHelper;
101
    }
102
103
    /**
104
     * Get theme names which are stock Mautic.
105
     *
106
     * @return array
107
     */
108
    public function getDefaultThemes()
109
    {
110
        return $this->defaultThemes;
111
    }
112
113
    /**
114
     * @param string $defaultTheme
115
     */
116
    public function setDefaultTheme($defaultTheme)
117
    {
118
        $this->defaultTheme = $defaultTheme;
119
    }
120
121
    /**
122
     * @param $themeName
123
     *
124
     * @return TemplatingThemeHelper
125
     *
126
     * @throws MauticException\BadConfigurationException
127
     * @throws MauticException\FileNotFoundException
128
     */
129
    public function createThemeHelper($themeName)
130
    {
131
        if ('current' === $themeName) {
132
            $themeName = $this->defaultTheme;
133
        }
134
135
        return new TemplatingThemeHelper($this->pathsHelper, $themeName);
136
    }
137
138
    /**
139
     * @param $newName
140
     *
141
     * @return string
142
     */
143
    private function getDirectoryName($newName)
144
    {
145
        return InputHelper::filename($newName);
146
    }
147
148
    /**
149
     * @param $theme
150
     *
151
     * @return bool
152
     */
153
    public function exists($theme)
154
    {
155
        $root    = $this->pathsHelper->getSystemPath('themes', true).'/';
156
        $dirName = $this->getDirectoryName($theme);
157
        $fs      = new Filesystem();
158
159
        return $fs->exists($root.$dirName);
160
    }
161
162
    /**
163
     * @param $theme
164
     * @param $newName
165
     *
166
     * @throws MauticException\FileExistsException
167
     * @throws MauticException\FileNotFoundException
168
     */
169
    public function copy($theme, $newName)
170
    {
171
        $root   = $this->pathsHelper->getSystemPath('themes', true).'/';
172
        $themes = $this->getInstalledThemes();
173
174
        //check to make sure the theme exists
175
        if (!isset($themes[$theme])) {
176
            throw new MauticException\FileNotFoundException($theme.' not found!');
177
        }
178
179
        $dirName = $this->getDirectoryName($newName);
180
181
        $fs = new Filesystem();
182
183
        if ($fs->exists($root.$dirName)) {
184
            throw new MauticException\FileExistsException("$dirName already exists");
185
        }
186
187
        $fs->mirror($root.$theme, $root.$dirName);
188
189
        $this->updateConfig($root.$dirName, $newName);
190
    }
191
192
    /**
193
     * @param $theme
194
     * @param $newName
195
     *
196
     * @throws MauticException\FileNotFoundException
197
     * @throws MauticException\FileExistsException
198
     */
199
    public function rename($theme, $newName)
200
    {
201
        $root   = $this->pathsHelper->getSystemPath('themes', true).'/';
202
        $themes = $this->getInstalledThemes();
203
204
        //check to make sure the theme exists
205
        if (!isset($themes[$theme])) {
206
            throw new MauticException\FileNotFoundException($theme.' not found!');
207
        }
208
209
        $dirName = $this->getDirectoryName($newName);
210
211
        $fs = new Filesystem();
212
213
        if ($fs->exists($root.$dirName)) {
214
            throw new MauticException\FileExistsException("$dirName already exists");
215
        }
216
217
        $fs->rename($root.$theme, $root.$dirName);
218
219
        $this->updateConfig($root.$theme, $dirName);
220
    }
221
222
    /**
223
     * @param $theme
224
     *
225
     * @throws MauticException\FileNotFoundException
226
     */
227
    public function delete($theme)
228
    {
229
        $root   = $this->pathsHelper->getSystemPath('themes', true).'/';
230
        $themes = $this->getInstalledThemes();
231
232
        //check to make sure the theme exists
233
        if (!isset($themes[$theme])) {
234
            throw new MauticException\FileNotFoundException($theme.' not found!');
235
        }
236
237
        $fs = new Filesystem();
238
        $fs->remove($root.$theme);
239
    }
240
241
    /**
242
     * Updates the theme configuration and converts
243
     * it to json if still using php array.
244
     *
245
     * @param $themePath
246
     * @param $newName
247
     */
248
    private function updateConfig($themePath, $newName)
249
    {
250
        if (file_exists($themePath.'/config.json')) {
251
            $config = json_decode(file_get_contents($themePath.'/config.json'), true);
252
        }
253
254
        $config['name'] = $newName;
255
256
        file_put_contents($themePath.'/config.json', json_encode($config));
257
    }
258
259
    /**
260
     * Fetches the optional settings from the defined steps.
261
     *
262
     * @return array
263
     */
264
    public function getOptionalSettings()
265
    {
266
        $minors = [];
267
268
        foreach ($this->steps as $step) {
269
            foreach ($step->checkOptionalSettings() as $minor) {
270
                $minors[] = $minor;
271
            }
272
        }
273
274
        return $minors;
275
    }
276
277
    /**
278
     * @param string $template
279
     *
280
     * @return string The logical name for the template
281
     */
282
    public function checkForTwigTemplate($template)
283
    {
284
        $parser     = $this->templatingHelper->getTemplateNameParser();
285
        $templating = $this->templatingHelper->getTemplating();
286
287
        $template = $parser->parse($template);
288
289
        $twigTemplate = clone $template;
290
        $twigTemplate->set('engine', 'twig');
291
292
        // Does a twig version exist?
293
        if ($templating->exists($twigTemplate)) {
294
            return $twigTemplate->getLogicalName();
295
        }
296
297
        // Does a PHP version exist?
298
        if ($templating->exists($template)) {
299
            return $template->getLogicalName();
300
        }
301
302
        // Try any theme as a fall back starting with default
303
        $this->findThemeWithTemplate($templating, $twigTemplate);
304
305
        return $twigTemplate->getLogicalName();
306
    }
307
308
    /**
309
     * @param string $specificFeature
310
     * @param bool   $extended        returns extended information about the themes
311
     * @param bool   $ignoreCache     true to get the fresh info
312
     * @param bool   $includeDirs     true to get the theme dir details
313
     *
314
     * @return mixed
315
     */
316
    public function getInstalledThemes($specificFeature = 'all', $extended = false, $ignoreCache = false, $includeDirs = true)
317
    {
318
        if (empty($this->themes[$specificFeature]) || $ignoreCache) {
319
            $dir    = $this->pathsHelper->getSystemPath('themes', true);
320
            $finder = new Finder();
321
            $finder->directories()->depth('0')->ignoreDotFiles(true)->in($dir);
322
323
            $this->themes[$specificFeature]     = [];
324
            $this->themesInfo[$specificFeature] = [];
325
            foreach ($finder as $theme) {
326
                if (!file_exists($theme->getRealPath().'/config.json')) {
327
                    continue;
328
                }
329
330
                $config = json_decode(file_get_contents($theme->getRealPath().'/config.json'), true);
331
332
                if ('all' === $specificFeature || (isset($config['features']) && in_array($specificFeature, $config['features']))) {
333
                    $this->themes[$specificFeature][$theme->getBasename()]               = $config['name'];
334
                    $this->themesInfo[$specificFeature][$theme->getBasename()]           = [];
335
                    $this->themesInfo[$specificFeature][$theme->getBasename()]['name']   = $config['name'];
336
                    $this->themesInfo[$specificFeature][$theme->getBasename()]['key']    = $theme->getBasename();
337
                    $this->themesInfo[$specificFeature][$theme->getBasename()]['config'] = $config;
338
339
                    if ($includeDirs) {
340
                        $this->themesInfo[$specificFeature][$theme->getBasename()]['dir']            = $theme->getRealPath();
341
                        $this->themesInfo[$specificFeature][$theme->getBasename()]['themesLocalDir'] = $this->pathsHelper->getSystemPath(
342
                            'themes',
343
                            false
344
                        );
345
                    }
346
                }
347
            }
348
        }
349
350
        if ($extended) {
351
            return $this->themesInfo[$specificFeature];
352
        }
353
354
        return $this->themes[$specificFeature];
355
    }
356
357
    /**
358
     * @param string $theme
359
     * @param bool   $throwException
360
     *
361
     * @return TemplatingThemeHelper
362
     *
363
     * @throws MauticException\FileNotFoundException
364
     * @throws MauticException\BadConfigurationException
365
     */
366
    public function getTheme($theme = 'current', $throwException = false)
367
    {
368
        if (empty($this->themeHelpers[$theme])) {
369
            try {
370
                $this->themeHelpers[$theme] = $this->createThemeHelper($theme);
371
            } catch (MauticException\FileNotFoundException $e) {
372
                if (!$throwException) {
373
                    // theme wasn't found so just use the first available
374
                    $themes = $this->getInstalledThemes();
375
376
                    foreach ($themes as $installedTheme => $name) {
377
                        try {
378
                            if (isset($this->themeHelpers[$installedTheme])) {
379
                                // theme found so return it
380
                                return $this->themeHelpers[$installedTheme];
381
                            } else {
382
                                $this->themeHelpers[$installedTheme] = $this->createThemeHelper($installedTheme);
383
                                // found so use this theme
384
                                $theme = $installedTheme;
385
                                $found = true;
386
                                break;
387
                            }
388
                        } catch (MauticException\FileNotFoundException $e) {
389
                            continue;
390
                        }
391
                    }
392
                }
393
394
                if (empty($found)) {
395
                    // if we get to this point then no template was found so throw an exception regardless
396
                    throw $e;
397
                }
398
            }
399
        }
400
401
        return $this->themeHelpers[$theme];
402
    }
403
404
    /**
405
     * Install a theme from a zip package.
406
     *
407
     * @param string $zipFile path
408
     *
409
     * @return bool
410
     *
411
     * @throws MauticException\FileNotFoundException
412
     * @throws \Exception
413
     */
414
    public function install($zipFile)
415
    {
416
        if (false === file_exists($zipFile)) {
417
            throw new MauticException\FileNotFoundException();
418
        }
419
420
        if (false === class_exists('ZipArchive')) {
421
            throw new \Exception('mautic.core.ziparchive.not.installed');
422
        }
423
424
        $themeName = basename($zipFile, '.zip');
425
426
        if (in_array($themeName, $this->getDefaultThemes())) {
427
            throw new \Exception($this->translator->trans('mautic.core.theme.default.cannot.overwrite', ['%name%' => $themeName], 'validators'));
428
        }
429
430
        $themePath = $this->pathsHelper->getSystemPath('themes', true).'/'.$themeName;
431
        $zipper    = new \ZipArchive();
432
        $archive   = $zipper->open($zipFile);
433
434
        if (true !== $archive) {
435
            throw new \Exception($this->getExtractError($archive));
436
        }
437
438
        $requiredFiles      = ['config.json', 'html/message.html.twig'];
439
        $foundRequiredFiles = [];
440
        $allowedFiles       = [];
441
        $allowedExtensions  = $this->coreParametersHelper->get('theme_import_allowed_extensions');
442
443
        $config = [];
444
        for ($i = 0; $i < $zipper->numFiles; ++$i) {
445
            $entry = $zipper->getNameIndex($i);
446
            if (0 === strpos($entry, '/')) {
447
                $entry = substr($entry, 1);
448
            }
449
450
            $extension = pathinfo($entry, PATHINFO_EXTENSION);
451
452
            // Check for required files
453
            if (in_array($entry, $requiredFiles)) {
454
                $foundRequiredFiles[] = $entry;
455
            }
456
457
            // Filter out dangerous files like .php
458
            if (empty($extension) || in_array(strtolower($extension), $allowedExtensions)) {
459
                $allowedFiles[] = $entry;
460
            }
461
462
            if ('config.json' === $entry) {
463
                $config = json_decode($zipper->getFromName($entry), true);
464
            }
465
        }
466
467
        if (!empty($config['features'])) {
468
            foreach ($config['features'] as $feature) {
469
                $featureFile     = sprintf('html/%s.html.twig', strtolower($feature));
470
                $requiredFiles[] = $featureFile;
471
472
                if (in_array($featureFile, $allowedFiles)) {
473
                    $foundRequiredFiles[] = $featureFile;
474
                }
475
            }
476
        }
477
478
        if ($missingFiles = array_diff($requiredFiles, $foundRequiredFiles)) {
479
            throw new MauticException\FileNotFoundException($this->translator->trans('mautic.core.theme.missing.files', ['%files%' => implode(', ', $missingFiles)], 'validators'));
480
        }
481
482
        // Extract the archive file now
483
        if (!$zipper->extractTo($themePath, $allowedFiles)) {
484
            throw new \Exception('mautic.core.update.error_extracting_package');
485
        } else {
486
            $zipper->close();
487
            unlink($zipFile);
488
489
            return true;
490
        }
491
    }
492
493
    /**
494
     * Get the error message from the zip archive.
495
     *
496
     * @param \ZipArchive $archive
497
     *
498
     * @return string
499
     */
500
    public function getExtractError($archive)
501
    {
502
        switch ($archive) {
503
            case \ZipArchive::ER_EXISTS:
504
                $error = 'mautic.core.update.archive_file_exists';
505
                break;
506
            case \ZipArchive::ER_INCONS:
507
            case \ZipArchive::ER_INVAL:
508
            case \ZipArchive::ER_MEMORY:
509
                $error = 'mautic.core.update.archive_zip_corrupt';
510
                break;
511
            case \ZipArchive::ER_NOENT:
512
                $error = 'mautic.core.update.archive_no_such_file';
513
                break;
514
            case \ZipArchive::ER_NOZIP:
515
                $error = 'mautic.core.update.archive_not_valid_zip';
516
                break;
517
            case \ZipArchive::ER_READ:
518
            case \ZipArchive::ER_SEEK:
519
            case \ZipArchive::ER_OPEN:
520
            default:
521
                $error = 'mautic.core.update.archive_could_not_open';
522
                break;
523
        }
524
525
        return $error;
526
    }
527
528
    /**
529
     * Creates a zip file from a theme and returns the path where it's stored.
530
     *
531
     * @param string $themeName
532
     *
533
     * @return string
534
     *
535
     * @throws Exception
536
     */
537
    public function zip($themeName)
538
    {
539
        $themePath = $this->pathsHelper->getSystemPath('themes', true).'/'.$themeName;
540
        $tmpPath   = $this->pathsHelper->getSystemPath('cache', true).'/tmp_'.$themeName.'.zip';
541
        $zipper    = new \ZipArchive();
542
        $finder    = new Finder();
543
544
        if (file_exists($tmpPath)) {
545
            @unlink($tmpPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

545
            /** @scrutinizer ignore-unhandled */ @unlink($tmpPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
546
        }
547
548
        $archive = $zipper->open($tmpPath, \ZipArchive::CREATE);
549
550
        $finder->files()->in($themePath);
551
552
        if (true !== $archive) {
553
            throw new \Exception($this->getExtractError($archive));
554
        } else {
555
            foreach ($finder as $file) {
556
                $filePath  = $file->getRealPath();
557
                $localPath = $file->getRelativePathname();
558
                $zipper->addFile($filePath, $localPath);
559
            }
560
            $zipper->close();
561
562
            return $tmpPath;
563
        }
564
565
        return false;
566
    }
567
568
    /**
569
     * @throws MauticException\BadConfigurationException
570
     * @throws MauticException\FileNotFoundException
571
     */
572
    private function findThemeWithTemplate(EngineInterface $templating, TemplateReference $template)
573
    {
574
        preg_match('/^:(.*?):(.*?)$/', $template->getLogicalName(), $match);
575
        $requestedThemeName = $match[1];
576
577
        // Try the default theme first
578
        $defaultTheme = $this->getTheme();
579
        if ($requestedThemeName !== $defaultTheme->getTheme()) {
580
            $template->set('controller', $defaultTheme->getTheme());
581
            if ($templating->exists($template)) {
582
                return;
583
            }
584
        }
585
586
        // Find any theme as a fallback
587
        $themes = $this->getInstalledThemes('all', true);
588
        foreach ($themes as $theme) {
589
            // Already handled the default
590
            if ($theme['key'] === $defaultTheme->getTheme()) {
591
                continue;
592
            }
593
594
            // Theme name is stored in the controller parameter
595
            $template->set('controller', $theme['key']);
596
597
            if ($templating->exists($template)) {
598
                return;
599
            }
600
        }
601
    }
602
}
603