Issues (281)

Branch: master

src/Backend/Modules/Extensions/Engine/Model.php (1 issue)

1
<?php
2
3
namespace Backend\Modules\Extensions\Engine;
4
5
use Backend\Modules\Locale\Engine\Model as BackendLocaleModel;
6
use Common\ModulesSettings;
7
use Symfony\Component\Filesystem\Filesystem;
8
use Symfony\Component\Finder\Finder;
9
use Backend\Core\Engine\Authentication as BackendAuthentication;
10
use Backend\Core\Engine\DataGridFunctions as BackendDataGridFunctions;
11
use Backend\Core\Engine\Exception;
12
use Backend\Core\Language\Language as BL;
13
use Backend\Core\Engine\Model as BackendModel;
14
15
/**
16
 * In this file we store all generic functions that we will be using in the extensions module.
17
 */
18
class Model
19
{
20
    /**
21
     * Overview of templates.
22
     *
23
     * @var string
24
     */
25
    const QUERY_BROWSE_TEMPLATES = 'SELECT i.id, i.label AS title
26
                                  FROM themes_templates AS i
27
                                  WHERE i.theme = ?
28
                                  ORDER BY i.label ASC';
29
30
    /**
31
     * Modules which are part of the core and can not be managed.
32
     *
33
     * @var array
34
     */
35
    private static $ignoredModules = [
36
        'Authentication',
37
        'Dashboard',
38
        'Error',
39
        'Extensions',
40
        'Settings',
41
    ];
42
43
    /**
44
     * Build HTML for a template (visual representation)
45
     *
46
     * @param string $format The template format.
47
     * @param bool $large Will the HTML be used in a large version?
48
     *
49
     * @return string
50
     */
51
    public static function buildTemplateHTML(string $format, bool $large = false): string
52
    {
53
        // cleanup
54
        $table = self::templateSyntaxToArray($format);
55
56
        // init var
57
        $rows = count($table);
58
        if ($rows === 0) {
59
            throw new Exception('Invalid template-format.');
60
        }
61
        $cells = count($table[0]);
62
63
        $htmlContent = [];
64
65
        // loop rows
66
        for ($y = 0; $y < $rows; ++$y) {
67
            $htmlContent[$y] = [];
68
69
            // loop cells
70
            for ($x = 0; $x < $cells; ++$x) {
71
                // skip if needed
72
                if (!isset($table[$y][$x])) {
73
                    continue;
74
                }
75
76
                // get value
77
                $value = $table[$y][$x];
78
79
                // init var
80
                $colspan = 1;
81
82
                // reset items in the same column
83
                while ($x + $colspan < $cells && $table[$y][$x + $colspan] === $value) {
84
                    $table[$y][$x + $colspan++] = null;
85
                }
86
87
                // init var
88
                $rowspan = 1;
89
                $rowMatches = true;
90
91
                // loop while the rows match
92
                while ($rowMatches && $y + $rowspan < $rows) {
93
                    // loop columns inside spanned columns
94
                    for ($i = 0; $i < $colspan; ++$i) {
95
                        // check value
96
                        if ($table[$y + $rowspan][$x + $i] !== $value) {
97
                            // no match, so stop
98
                            $rowMatches = false;
99
                            break;
100
                        }
101
                    }
102
103
                    // any rowmatches?
104
                    if ($rowMatches) {
105
                        // loop columns and reset value
106
                        for ($i = 0; $i < $colspan; ++$i) {
107
                            $table[$y + $rowspan][$x + $i] = null;
108
                        }
109
110
                        // increment
111
                        ++$rowspan;
112
                    }
113
                }
114
115
                $htmlContent[$y][$x] = [
116
                    'title' => \SpoonFilter::ucfirst($value),
117
                    'value' => $value,
118
                    'exists' => $value != '/',
119
                    'rowspan' => $rowspan,
120
                    'colspan' => $colspan,
121
                    'large' => $large,
122
                ];
123
            }
124
        }
125
126
        $templating = BackendModel::get('template');
127
        $templating->assign('table', $htmlContent);
128
        $html = $templating->getContent('Extensions/Layout/Templates/Templates.html.twig');
129
130
        return $html;
131
    }
132
133
    /**
134
     * Checks the settings and optionally returns an array with warnings
135
     *
136
     * @return array
137
     */
138 2
    public static function checkSettings(): array
139
    {
140 2
        $warnings = [];
141 2
        $akismetModules = self::getModulesThatRequireAkismet();
142 2
        $googleMapsModules = self::getModulesThatRequireGoogleMaps();
143
144
        // check if this action is allowed
145 2
        if (!BackendAuthentication::isAllowedAction('Index', 'Settings')) {
146
            return [];
147
        }
148
149
        // check if the akismet key is available if there are modules that require it
150 2
        if (!empty($akismetModules) && BackendModel::get('fork.settings')->get('Core', 'akismet_key', null) == '') {
151
            // add warning
152 2
            $warnings[] = [
153 2
                'message' => sprintf(
154 2
                    BL::err('AkismetKey'),
155 2
                    BackendModel::createUrlForAction('Index', 'Settings')
156
                ),
157
            ];
158
        }
159
160
        // check if the google maps key is available if there are modules that require it
161 2
        if (!empty($googleMapsModules)
162 2
            && BackendModel::get('fork.settings')->get('Core', 'google_maps_key', null) == '') {
163
            // add warning
164
            $warnings[] = [
165
                'message' => sprintf(
166
                    BL::err('GoogleMapsKey'),
167
                    BackendModel::createUrlForAction('Index', 'Settings')
168
                ),
169
            ];
170
        }
171
172 2
        return $warnings;
173
    }
174
175
    /**
176
     * Clear all applications cache.
177
     *
178
     * Note: we do not need to rebuild anything, the core will do this when noticing the cache files are missing.
179
     */
180
    public static function clearCache(): void
181
    {
182
        $finder = new Finder();
183
        $filesystem = new Filesystem();
184
        $files = $finder->files()
185
            ->name('*.php')
186
            ->name('*.js')
187
            ->in(BACKEND_CACHE_PATH . '/Locale')
188
            ->in(FRONTEND_CACHE_PATH . '/Navigation')
189
            ->in(FRONTEND_CACHE_PATH . '/Locale');
190
        foreach ($files as $file) {
191
            $filesystem->remove($file->getRealPath());
192
        }
193
        BackendModel::getContainer()->get('cache.backend_navigation')->delete();
194
    }
195
196
    /**
197
     * Delete a template.
198
     *
199
     * @param int $id The id of the template to delete.
200
     *
201
     * @return bool
202
     */
203
    public static function deleteTemplate(int $id): bool
204
    {
205
        $templates = self::getTemplates();
206
207
        // we can't delete a template that doesn't exist
208
        if (!isset($templates[$id])) {
209
            return false;
210
        }
211
212
        // we can't delete the last template
213
        if (count($templates) === 1) {
214
            return false;
215
        }
216
217
        // we can't delete the default template
218
        if ($id == BackendModel::get('fork.settings')->get('Pages', 'default_template')) {
219
            return false;
220
        }
221
        if (self::isTemplateInUse($id)) {
222
            return false;
223
        }
224
225
        $database = BackendModel::getContainer()->get('database');
226
        $database->delete('themes_templates', 'id = ?', $id);
227
        $ids = (array) $database->getColumn(
228
            'SELECT i.revision_id
229
             FROM pages AS i
230
             WHERE i.template_id = ? AND i.status != ?',
231
            [$id, 'active']
232
        );
233
234
        if (!empty($ids)) {
235
            // delete those pages and the linked blocks
236
            $database->delete('pages', 'revision_id IN(' . implode(',', $ids) . ')');
237
            $database->delete('pages_blocks', 'revision_id IN(' . implode(',', $ids) . ')');
238
        }
239
240
        return true;
241
    }
242
243
    /**
244
     * Does this module exist.
245
     * This does not check for existence in the database but on the filesystem.
246
     *
247
     * @param string $module Module to check for existence.
248
     *
249
     * @return bool
250
     */
251 1
    public static function existsModule(string $module): bool
252
    {
253 1
        return is_dir(BACKEND_MODULES_PATH . '/' . $module);
254
    }
255
256
    /**
257
     * Check if a template exists
258
     *
259
     * @param int $id The Id of the template to check for existence.
260
     *
261
     * @return bool
262
     */
263 1
    public static function existsTemplate(int $id): bool
264
    {
265 1
        return (bool) BackendModel::getContainer()->get('database')->getVar(
266 1
            'SELECT i.id FROM themes_templates AS i WHERE i.id = ?',
267 1
            [$id]
268
        );
269
    }
270
271
    /**
272
     * Does this template exist.
273
     * This does not check for existence in the database but on the filesystem.
274
     *
275
     * @param string $theme Theme to check for existence.
276
     *
277
     * @return bool
278
     */
279 3
    public static function existsTheme(string $theme): bool
280
    {
281 3
        return is_dir(FRONTEND_PATH . '/Themes/' . (string) $theme) || $theme === 'Core';
282
    }
283
284 2
    public static function getExtras(): array
285
    {
286 2
        $extras = (array) BackendModel::getContainer()->get('database')->getRecords(
287 2
            'SELECT i.id, i.module, i.type, i.label, i.data
288
             FROM modules_extras AS i
289
             INNER JOIN modules AS m ON i.module = m.name
290
             WHERE i.hidden = ?
291
             ORDER BY i.module, i.sequence',
292 2
            [false],
293 2
            'id'
294
        );
295 2
        $itemsToRemove = [];
296
297 2
        foreach ($extras as $id => &$row) {
298 2
            $row['data'] = $row['data'] === null ? [] : @unserialize($row['data'], ['allowed_classes' => false]);
299 2
            if (isset($row['data']['language']) && $row['data']['language'] != BL::getWorkingLanguage()) {
300
                $itemsToRemove[] = $id;
301
            }
302
303
            // set URL if needed, we use '' instead of null, because otherwise the module of the current action (modules) is used.
304 2
            if (!isset($row['data']['url'])) {
305 2
                $row['data']['url'] = BackendModel::createUrlForAction('', $row['module']);
306
            }
307
308 2
            $name = \SpoonFilter::ucfirst(BL::lbl($row['label']));
309 2
            if (isset($row['data']['extra_label'])) {
310
                $name = $row['data']['extra_label'];
311
            }
312 2
            if (isset($row['data']['label_variables'])) {
313
                $name = vsprintf($name, $row['data']['label_variables']);
314
            }
315
316
            // add human readable name
317 2
            $module = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase($row['module'])));
318 2
            $extraTypeLabel = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase('ExtraType_' . $row['type'])));
319 2
            $row['human_name'] = $extraTypeLabel . ': ' . $name;
320 2
            $row['path'] = $extraTypeLabel . ' › ' . $module . ($module !== $name ? ' › ' . $name : '');
321
        }
322
323
        // any items to remove?
324 2
        if (!empty($itemsToRemove)) {
325
            foreach ($itemsToRemove as $id) {
326
                unset($extras[$id]);
327
            }
328
        }
329
330 2
        return $extras;
331
    }
332
333
    public static function getExtrasData(): array
334
    {
335
        $extras = (array) BackendModel::getContainer()->get('database')->getRecords(
336
            'SELECT i.id, i.module, i.type, i.label, i.data
337
             FROM modules_extras AS i
338
             INNER JOIN modules AS m ON i.module = m.name
339
             WHERE i.hidden = ?
340
             ORDER BY i.module, i.sequence',
341
            [false]
342
        );
343
        $values = [];
344
345
        foreach ($extras as $row) {
346
            $row['data'] = @unserialize($row['data'], ['allowed_classes' => false]);
347
348
            // remove items that are not for the current language
349
            if (isset($row['data']['language']) && $row['data']['language'] != BL::getWorkingLanguage()) {
350
                continue;
351
            }
352
353
            // set URL if needed
354
            if (!isset($row['data']['url'])) {
355
                $row['data']['url'] = BackendModel::createUrlForAction(
356
                    'Index',
357
                    $row['module']
358
                );
359
            }
360
361
            $name = \SpoonFilter::ucfirst(BL::lbl($row['label']));
362
            if (isset($row['data']['extra_label'])) {
363
                $name = $row['data']['extra_label'];
364
            }
365
            if (isset($row['data']['label_variables'])) {
366
                $name = vsprintf($name, $row['data']['label_variables']);
367
            }
368
            $moduleName = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase($row['module'])));
369
370
            if (!isset($values[$row['module']])) {
371
                $values[$row['module']] = [
372
                    'value' => $row['module'],
373
                    'name' => $moduleName,
374
                    'items' => [],
375
                ];
376
            }
377
378
            $values[$row['module']]['items'][$row['type']][$name] = ['id' => $row['id'], 'label' => $name];
379
        }
380
381
        return $values;
382
    }
383
384
    /**
385
     * Fetch the module information from the info.xml file.
386
     *
387
     * @param string $module
388
     *
389
     * @return array
390
     */
391 2
    public static function getModuleInformation(string $module): array
392
    {
393 2
        $pathInfoXml = BACKEND_MODULES_PATH . '/' . $module . '/info.xml';
394 2
        $information = ['data' => [], 'warnings' => []];
395
396 2
        if (is_file($pathInfoXml)) {
397
            try {
398 2
                $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
399 2
                $information['data'] = self::processModuleXml($infoXml);
400 2
                if (empty($information['data'])) {
401
                    $information['warnings'][] = [
402 2
                        'message' => BL::getMessage('InformationFileIsEmpty'),
403
                    ];
404
                }
405
            } catch (Exception $e) {
406
                $information['warnings'][] = [
407 2
                    'message' => BL::getMessage('InformationFileCouldNotBeLoaded'),
408
                ];
409
            }
410
        } else {
411
            $information['warnings'][] = [
412
                'message' => BL::getMessage('InformationFileIsMissing'),
413
            ];
414
        }
415
416 2
        return $information;
417
    }
418
419
    /**
420
     * Get modules based on the directory listing in the backend application.
421
     *
422
     * If a module contains a info.xml it will be parsed.
423
     *
424
     * @return array
425
     */
426 1
    public static function getModules(): array
427
    {
428 1
        $installedModules = (array) BackendModel::getContainer()
429 1
            ->getParameter('installed_modules');
430 1
        $modules = BackendModel::getModulesOnFilesystem(false);
431 1
        $manageableModules = [];
432
433
        // get more information for each module
434 1
        foreach ($modules as $moduleName) {
435 1
            if (in_array($moduleName, self::$ignoredModules)) {
436 1
                continue;
437
            }
438
439 1
            $module = [];
440 1
            $module['id'] = 'module_' . $moduleName;
441 1
            $module['raw_name'] = $moduleName;
442 1
            $module['name'] = \SpoonFilter::ucfirst(BL::getLabel(\SpoonFilter::toCamelCase($moduleName)));
443 1
            $module['description'] = '';
444 1
            $module['version'] = '';
445 1
            $module['installed'] = false;
446
447 1
            if (in_array($moduleName, $installedModules)) {
448 1
                $module['installed'] = true;
449
            }
450
451
            try {
452 1
                $infoXml = @new \SimpleXMLElement(
453 1
                    BACKEND_MODULES_PATH . '/' . $module['raw_name'] . '/info.xml',
454 1
                    LIBXML_NOCDATA,
455 1
                    true
456
                );
457
458 1
                $info = self::processModuleXml($infoXml);
459
460
                // set fields if they were found in the XML
461 1
                if (isset($info['description'])) {
462 1
                    $module['description'] = BackendDataGridFunctions::truncate($info['description'], 80);
463
                }
464 1
                if (isset($info['version'])) {
465 1
                    $module['version'] = $info['version'];
466
                }
467
            } catch (\Exception $e) {
468
                // don't act upon error, we simply won't possess some info
469
            }
470
471 1
            $manageableModules[] = $module;
472
        }
473
474 1
        return $manageableModules;
475
    }
476
477
    /**
478
     * Fetch the list of modules that require Akismet API key
479
     *
480
     * @return array
481
     */
482 2
    public static function getModulesThatRequireAkismet(): array
483
    {
484 2
        return self::getModulesThatRequireSetting('akismet');
485
    }
486
487
    /**
488
     * Fetch the list of modules that require Google Maps API key
489
     *
490
     * @return array
491
     */
492 2
    public static function getModulesThatRequireGoogleMaps(): array
493
    {
494 2
        return self::getModulesThatRequireSetting('google_maps');
495
    }
496
497
    /**
498
     * Fetch the list of modules that require Google Recaptcha API key
499
     *
500
     * @return array
501
     */
502
    public static function getModulesThatRequireGoogleRecaptcha(): array
503
    {
504
        return self::getModulesThatRequireSetting('google_recaptcha');
505
    }
506
507
    /**
508
     * Fetch the list of modules that require a certain setting. The setting is affixed by 'requires_'
509
     *
510
     * @param string $setting
511
     *
512
     * @return array
513
     */
514 2
    private static function getModulesThatRequireSetting(string $setting): array
515
    {
516 2
        if ($setting === '') {
517
            return [];
518
        }
519
520
        /** @var ModulesSettings $moduleSettings */
521 2
        $moduleSettings = BackendModel::get('fork.settings');
522
523 2
        return array_filter(
524 2
            BackendModel::getModules(),
525
            function (string $module) use ($moduleSettings, $setting): bool {
526 2
                return $moduleSettings->get($module, 'requires_' . $setting, false);
527 2
            }
528
        );
529
    }
530
531 2
    public static function getTemplate(int $id): array
532
    {
533 2
        return (array) BackendModel::getContainer()->get('database')->getRecord(
534 2
            'SELECT i.* FROM themes_templates AS i WHERE i.id = ?',
535 2
            [$id]
536
        );
537
    }
538
539
    public static function getTemplates(string $theme = null): array
540
    {
541
        $database = BackendModel::getContainer()->get('database');
542
        $theme = \SpoonFilter::getValue(
543
            (string) $theme,
544
            null,
545
            BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork')
546
        );
547
548
        $templates = (array) $database->getRecords(
549
            'SELECT i.id, i.label, i.path, i.data
550
            FROM themes_templates AS i
551
            WHERE i.theme = ? AND i.active = ?
552
            ORDER BY i.label ASC',
553
            [$theme, true],
554
            'id'
555
        );
556
557
        $extras = (array) self::getExtras();
558
        $half = (int) ceil(count($templates) / 2);
559
        $i = 0;
560
561
        foreach ($templates as &$row) {
562
            $row['data'] = unserialize($row['data'], ['allowed_classes' => false]);
563
            $row['has_block'] = false;
564
565
            // reset
566
            if (isset($row['data']['default_extras_' . BL::getWorkingLanguage()])) {
567
                $row['data']['default_extras'] = $row['data']['default_extras_' . BL::getWorkingLanguage()];
568
            }
569
570
            // any extras?
571
            if (isset($row['data']['default_extras'])) {
572
                foreach ($row['data']['default_extras'] as $value) {
573
                    if (\SpoonFilter::isInteger($value)
574
                        && isset($extras[$value]) && $extras[$value]['type'] == 'block'
575
                    ) {
576
                        $row['has_block'] = true;
577
                    }
578
                }
579
            }
580
581
            // validate
582
            if (!isset($row['data']['format'])) {
583
                throw new Exception('Invalid template-format.');
584
            }
585
586
            $row['html'] = self::buildTemplateHTML($row['data']['format']);
587
            $row['htmlLarge'] = self::buildTemplateHTML($row['data']['format'], true);
588
            $row['json'] = json_encode($row);
589
            if ($i == $half) {
590
                $row['break'] = true;
591
            }
592
            ++$i;
593
        }
594
595
        return (array) $templates;
596
    }
597
598 7
    public static function getThemes(): array
599
    {
600 7
        $records = [];
601 7
        $finder = new Finder();
602 7
        foreach ($finder->directories()->in(FRONTEND_PATH . '/Themes')->depth(0) as $directory) {
603 7
            $pathInfoXml = BackendModel::getContainer()->getParameter('site.path_www') . '/src/Frontend/Themes/'
604 7
                           . $directory->getBasename() . '/info.xml';
605 7
            if (!is_file($pathInfoXml)) {
606
                throw new Exception('info.xml is missing for the theme ' . $directory->getBasename());
607
            }
608
            try {
609 7
                $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
610 7
                $information = self::processThemeXml($infoXml);
611 7
                if (empty($information)) {
612 7
                    throw new Exception('Invalid info.xml');
613
                }
614
            } catch (Exception $e) {
615
                $information['thumbnail'] = 'thumbnail.png';
616
            }
617
618 7
            $item = [];
619 7
            $item['value'] = $directory->getBasename();
620 7
            $item['label'] = $directory->getBasename();
621 7
            $item['thumbnail'] = '/src/Frontend/Themes/' . $item['value'] . '/' . $information['thumbnail'];
622 7
            $item['installed'] = self::isThemeInstalled($item['value']);
623 7
            $item['installable'] = isset($information['templates']);
624
625 7
            $records[$item['value']] = $item;
626
        }
627
628 7
        return (array) $records;
629
    }
630
631 1
    public static function createTemplateXmlForExport(string $theme): string
632
    {
633 1
        $charset = BackendModel::getContainer()->getParameter('kernel.charset');
634
635
        // build xml
636 1
        $xml = new \DOMDocument('1.0', $charset);
637 1
        $xml->preserveWhiteSpace = false;
638 1
        $xml->formatOutput = true;
639
640 1
        $root = $xml->createElement('templates');
641 1
        $xml->appendChild($root);
642
643 1
        $database = BackendModel::getContainer()->get('database');
644
645 1
        $records = $database->getRecords(self::QUERY_BROWSE_TEMPLATES, [$theme]);
646
647 1
        foreach ($records as $row) {
648 1
            $template = self::getTemplate($row['id']);
649 1
            $data = unserialize($template['data'], ['allowed_classes' => false]);
650
651 1
            $templateElement = $xml->createElement('template');
652 1
            $templateElement->setAttribute('label', $template['label']);
653 1
            $templateElement->setAttribute('path', $template['path']);
654 1
            $root->appendChild($templateElement);
655
656 1
            $positionsElement = $xml->createElement('positions');
657 1
            $templateElement->appendChild($positionsElement);
658
659 1
            foreach ($data['names'] as $name) {
660 1
                $positionElement = $xml->createElement('position');
661 1
                $positionElement->setAttribute('name', $name);
662 1
                $positionsElement->appendChild($positionElement);
663
            }
664
665 1
            $formatElement = $xml->createElement('format');
666 1
            $templateElement->appendChild($formatElement);
667 1
            $formatElement->nodeValue = $data['format'];
668
        }
669
670 1
        return $xml->saveXML();
671
    }
672
673 1
    public static function hasModuleWarnings(string $module): string
674
    {
675 1
        $moduleInformation = self::getModuleInformation($module);
676
677 1
        return !empty($moduleInformation['warnings']);
678
    }
679
680
    public static function insertTemplate(array $template): int
681
    {
682
        return (int) BackendModel::getContainer()->get('database')->insert('themes_templates', $template);
683
    }
684
685
    public static function installModule(string $module): void
686
    {
687
        $class = 'Backend\\Modules\\' . $module . '\\Installer\\Installer';
688
        $variables = [];
689
690
        // run installer
691
        $installer = new $class(
692
            BackendModel::getContainer()->get('database'),
693
            BL::getActiveLanguages(),
694
            array_keys(BL::getInterfaceLanguages()),
695
            false,
696
            $variables
697
        );
698
699
        $installer->install();
700
701
        // clear the cache so locale (and so much more) gets rebuilt
702
        self::clearCache();
703
    }
704
705 2
    public static function installTheme(string $theme): void
706
    {
707 2
        $basePath = FRONTEND_PATH . '/Themes/' . $theme;
708 2
        $pathInfoXml = $basePath . '/info.xml';
709 2
        $pathTranslations = $basePath . '/locale.xml';
710 2
        $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
711
712 2
        $information = self::processThemeXml($infoXml);
713 2
        if (empty($information)) {
714
            throw new Exception('Invalid info.xml');
715
        }
716
717 2
        if (is_file($pathTranslations)) {
718
            $translations = @simplexml_load_file($pathTranslations);
719
            if ($translations !== false) {
720
                BackendLocaleModel::importXML($translations);
721
            }
722
        }
723
724 2
        foreach ($information['templates'] as $template) {
725
            $item = [];
726
            $item['theme'] = $information['name'];
727
            $item['label'] = $template['label'];
728
            $item['path'] = $template['path'];
729
            $item['active'] = true;
730
            $item['data']['format'] = $template['format'];
731
            $item['data']['image'] = $template['image'];
732
733
            // build positions
734
            $item['data']['names'] = [];
735
            $item['data']['default_extras'] = [];
736
            foreach ($template['positions'] as $position) {
737
                $item['data']['names'][] = $position['name'];
738
                $item['data']['default_extras'][$position['name']] = [];
739
740
                // add default widgets
741
                foreach ($position['widgets'] as $widget) {
742
                    // fetch extra_id for this extra
743
                    $extraId = (int) BackendModel::getContainer()->get('database')->getVar(
744
                        'SELECT i.id
745
                         FROM modules_extras AS i
746
                         WHERE type = ? AND module = ? AND action = ? AND data IS NULL AND hidden = ?',
747
                        ['widget', $widget['module'], $widget['action'], false]
748
                    );
749
750
                    // add extra to defaults
751
                    if ($extraId) {
752
                        $item['data']['default_extras'][$position['name']][] = $extraId;
753
                    }
754
                }
755
756
                // add default editors
757
                foreach ($position['editors'] as $editor) {
758
                    $item['data']['default_extras'][$position['name']][] = 0;
759
                }
760
            }
761
762
            $item['data'] = serialize($item['data']);
763
            $item['id'] = self::insertTemplate($item);
764
        }
765 2
    }
766
767
    public static function isModuleInstalled(string $module): bool
768
    {
769
        return (bool) BackendModel::getContainer()->get('database')->getVar(
770
            'SELECT 1
771
             FROM modules
772
             WHERE name = ?
773
             LIMIT 1',
774
            $module
775
        );
776
    }
777
778
    /**
779
     * Is the provided template id in use by active versions of pages?
780
     *
781
     * @param int $templateId The id of the template to check.
782
     *
783
     * @return bool
784
     */
785 1
    public static function isTemplateInUse(int $templateId): bool
786
    {
787 1
        return (bool) BackendModel::getContainer()->get('database')->getVar(
788 1
            'SELECT 1
789
             FROM pages AS i
790
             WHERE i.template_id = ? AND i.status = ?
791
             LIMIT 1',
792 1
            [$templateId, 'active']
793
        );
794
    }
795
796 8
    public static function isThemeInstalled(string $theme): bool
797
    {
798 8
        return (bool) BackendModeL::getContainer()->get('database')->getVar(
799 8
            'SELECT 1
800
             FROM themes_templates
801
             WHERE theme = ?
802
             LIMIT 1',
803 8
            [$theme]
804
        );
805
    }
806
807
    /**
808
     * Check if a directory is writable.
809
     * The default is_writable function has problems due to Windows ACLs "bug"
810
     *
811
     * @param string $path The path to check.
812
     *
813
     * @return bool
814
     */
815 5
    public static function isWritable(string $path): bool
816
    {
817 5
        $path = rtrim((string) $path, '/');
818 5
        $file = uniqid('', true) . '.tmp';
819 5
        $return = @file_put_contents($path . '/' . $file, 'temporary file', FILE_APPEND);
820 5
        if ($return === false) {
821
            return false;
822
        }
823 5
        unlink($path . '/' . $file);
824
825 5
        return true;
826
    }
827
828 2
    public static function processModuleXml(\SimpleXMLElement $xml): array
829
    {
830 2
        $information = [];
831
832
        // fetch theme node
833 2
        $module = $xml->xpath('/module');
834 2
        if (isset($module[0])) {
835 2
            $module = $module[0];
836
        }
837
838
        // fetch general module info
839 2
        $information['name'] = (string) $module->name;
840 2
        $information['version'] = (string) $module->version;
841 2
        $information['requirements'] = (array) $module->requirements;
842 2
        $information['description'] = strip_tags((string) $module->description, '<h1><h2><h3><h4><h5><h6><p><li><a>');
843 2
        $information['cronjobs'] = [];
844
845
        // authors
846 2
        foreach ($xml->xpath('/module/authors/author') as $author) {
847 2
            $information['authors'][] = (array) $author;
848
        }
849
850
        // cronjobs
851 2
        foreach ($xml->xpath('/module/cronjobs/cronjob') as $cronjob) {
852
            $attributes = $cronjob->attributes();
853
            if (!isset($attributes['action'])) {
854
                continue;
855
            }
856
857
            // build cronjob information
858
            $item = [];
859
            $item['minute'] = (isset($attributes['minute'])) ? $attributes['minute'] : '*';
860
            $item['hour'] = (isset($attributes['hour'])) ? $attributes['hour'] : '*';
861
            $item['day-of-month'] = (isset($attributes['day-of-month'])) ? $attributes['day-of-month'] : '*';
862
            $item['month'] = (isset($attributes['month'])) ? $attributes['month'] : '*';
863
            $item['day-of-week'] = (isset($attributes['day-of-week'])) ? $attributes['day-of-week'] : '*';
864
            $item['action'] = $attributes['action'];
865
            $item['description'] = $cronjob[0];
866
867
            // check if cronjob has already been run
868
            $cronjobs = (array) BackendModel::get('fork.settings')->get('Core', 'cronjobs');
869
            $item['active'] = in_array($information['name'] . '.' . $attributes['action'], $cronjobs);
870
871
            $information['cronjobs'][] = $item;
872
        }
873
874
        // events
875 2
        foreach ($xml->xpath('/module/events/event') as $event) {
876 1
            $attributes = $event->attributes();
877
878
            // build event information and add it to the list
879 1
            $information['events'][] = [
880 1
                'application' => (isset($attributes['application'])) ? $attributes['application'] : '',
881 1
                'name' => (isset($attributes['name'])) ? $attributes['name'] : '',
882 1
                'description' => $event[0],
883
            ];
884
        }
885
886 2
        return $information;
887
    }
888
889 8
    public static function processThemeXml(\SimpleXMLElement $xml): array
890
    {
891 8
        $information = [];
892
893 8
        $theme = $xml->xpath('/theme');
894 8
        if (isset($theme[0])) {
895 8
            $theme = $theme[0];
896
        }
897
898
        // fetch general theme info
899 8
        $information['name'] = (string) $theme->name;
900 8
        $information['version'] = (string) $theme->version;
901 8
        $information['requirements'] = (array) $theme->requirements;
902 8
        $information['thumbnail'] = (string) $theme->thumbnail;
903 8
        $information['description'] = strip_tags((string) $theme->description, '<h1><h2><h3><h4><h5><h6><p><li><a>');
904
905
        // authors
906 8
        foreach ($xml->xpath('/theme/authors/author') as $author) {
907 2
            $information['authors'][] = (array) $author;
908
        }
909
910
        // meta navigation
911 8
        $meta = $theme->metanavigation->attributes();
912 8
        if (isset($meta['supported'])) {
913 8
            $information['meta'] = (string) $meta['supported'] && (string) $meta['supported'] !== 'false';
914
        }
915
916
        // templates
917 8
        $information['templates'] = [];
918 8
        foreach ($xml->xpath('/theme/templates/template') as $templateXML) {
919 8
            $template = [];
920
921
            // template data
922 8
            $template['label'] = (string) $templateXML['label'];
923 8
            $template['path'] = (string) $templateXML['path'];
924 8
            $template['image'] = isset($templateXML['image'])
925 8
                ? (string) $templateXML['image'] && (string) $templateXML['image'] !== 'false' : false;
926 8
            $template['format'] = trim(str_replace(["\n", "\r", ' '], '', (string) $templateXML->format));
927
928
            // loop positions
929 8
            foreach ($templateXML->positions->position as $positionXML) {
930 8
                $position = [];
931
932 8
                $position['name'] = (string) $positionXML['name'];
933
934
                // widgets
935 8
                $position['widgets'] = [];
936 8
                if ($positionXML->defaults->widget) {
937
                    foreach ($positionXML->defaults->widget as $widget) {
938
                        $position['widgets'][] = [
939
                            'module' => (string) $widget['module'],
940
                            'action' => (string) $widget['action'],
941
                        ];
942
                    }
943
                }
944
945
                // editor
946 8
                $position['editors'] = [];
947 8
                if ($positionXML->defaults->editor) {
948
                    foreach ($positionXML->defaults->editor as $editor) {
949
                        $position['editors'][] = (string) trim($editor);
0 ignored issues
show
It seems like $editor can also be of type null; however, parameter $string of trim() 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

949
                        $position['editors'][] = (string) trim(/** @scrutinizer ignore-type */ $editor);
Loading history...
950
                    }
951
                }
952
953 8
                $template['positions'][] = $position;
954
            }
955
956 8
            $information['templates'][] = $template;
957
        }
958
959 8
        return self::validateThemeInformation($information);
960
    }
961
962
    public static function templateSyntaxToArray(string $syntax): array
963
    {
964
        $syntax = (string) $syntax;
965
        $syntax = trim(str_replace(["\n", "\r", ' '], '', $syntax));
966
        $table = [];
967
968
        // check template settings format
969
        if (!static::isValidTemplateSyntaxFormat($syntax)) {
970
            return $table;
971
        }
972
973
        // split into rows
974
        $rows = explode('],[', $syntax);
975
976
        foreach ($rows as $i => $row) {
977
            $row = trim(str_replace(['[', ']'], '', $row));
978
            $table[$i] = (array) explode(',', $row);
979
        }
980
981
        if (!isset($table[0])) {
982
            return [];
983
        }
984
985
        $columns = count($table[0]);
986
987
        foreach ($table as $row) {
988
            if (count($row) !== $columns) {
989
                return [];
990
            }
991
        }
992
993
        return $table;
994
    }
995
996
    /**
997
     * Validate template syntax format
998
     *
999
     * @param string $syntax
1000
     * @return bool
1001
     */
1002
    public static function isValidTemplateSyntaxFormat(string $syntax): bool
1003
    {
1004
        return \SpoonFilter::isValidAgainstRegexp(
1005
            '/^\[(\/|[a-z0-9])+(,(\/|[a-z0-9]+))*\](,\[(\/|[a-z0-9])+(,(\/|[a-z0-9]+))*\])*$/i',
1006
            $syntax
1007
        );
1008
    }
1009
1010
    public static function updateTemplate(array $templateData): void
1011
    {
1012
        BackendModel::getContainer()->get('database')->update(
1013
            'themes_templates',
1014
            $templateData,
1015
            'id = ?',
1016
            [(int) $templateData['id']]
1017
        );
1018
    }
1019
1020
    /**
1021
     * Make sure that we have an entirely valid theme information array
1022
     *
1023
     * @param array $information Contains the parsed theme info.xml data.
1024
     *
1025
     * @return array
1026
     */
1027 8
    public static function validateThemeInformation(array $information): array
1028
    {
1029
        // set default thumbnail if not sets
1030 8
        if (!$information['thumbnail']) {
1031
            $information['thumbnail'] = 'thumbnail.png';
1032
        }
1033
1034
        // check if there are templates
1035 8
        if (isset($information['templates']) && $information['templates']) {
1036 8
            foreach ($information['templates'] as $i => $template) {
1037 8
                if (!isset($template['label']) || !$template['label'] || !isset($template['path']) || !$template['path'] || !isset($template['format']) || !$template['format']) {
1038
                    unset($information['templates'][$i]);
1039
                    continue;
1040
                }
1041
1042
                // if there are no positions we should continue with the next item
1043 8
                if (!isset($template['positions']) && $template['positions']) {
1044
                    continue;
1045
                }
1046
1047
                // loop positions
1048 8
                foreach ($template['positions'] as $j => $position) {
1049 8
                    if (!isset($position['name']) || !$position['name']) {
1050
                        unset($information['templates'][$i]['positions'][$j]);
1051
                        continue;
1052
                    }
1053
1054
                    // ensure widgets are well-formed
1055 8
                    if (!isset($position['widgets']) || !$position['widgets']) {
1056 8
                        $information['templates'][$i]['positions'][$j]['widgets'] = [];
1057
                    }
1058
1059
                    // ensure editors are well-formed
1060 8
                    if (!isset($position['editors']) || !$position['editors']) {
1061 8
                        $information['templates'][$i]['positions'][$j]['editors'] = [];
1062
                    }
1063
1064
                    // loop widgets
1065 8
                    foreach ($position['widgets'] as $k => $widget) {
1066
                        // check if widget is valid
1067
                        if (!isset($widget['module']) || !$widget['module'] || !isset($widget['action']) || !$widget['action']) {
1068
                            unset($information['templates'][$i]['positions'][$j]['widgets'][$k]);
1069 8
                            continue;
1070
                        }
1071
                    }
1072
                }
1073
1074
                // check if there still are valid positions
1075 8
                if (!isset($information['templates'][$i]['positions']) || !$information['templates'][$i]['positions']) {
1076 8
                    return [];
1077
                }
1078
            }
1079
1080
            // check if there still are valid templates
1081 8
            if (!isset($information['templates']) || !$information['templates']) {
1082
                return [];
1083
            }
1084
        }
1085
1086 8
        return $information;
1087
    }
1088
}
1089