Completed
Pull Request — master (#2233)
by Revin
64:12
created

ModuleInstaller   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 1071
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 7

Test Coverage

Coverage 94.4%

Importance

Changes 0
Metric Value
dl 0
loc 1071
c 0
b 0
f 0
wmc 80
lcom 3
cbo 7
ccs 337
cts 357
cp 0.944
rs 4.4196

45 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A getInput() 0 4 1
A setInput() 0 4 1
A getOutput() 0 4 1
A setOutput() 0 4 1
A getPromptVariables() 0 4 1
A addDefaultExtra() 0 4 1
A addModule() 0 21 2
B addSearchIndex() 0 43 6
A execute() 0 4 1
A getDatabase() 0 4 1
A getModule() 0 4 1
A setModule() 0 4 1
A getDefaultExtras() 0 4 1
A getDefaultUserID() 0 15 2
A getInterfaceLanguages() 0 4 1
A getLanguages() 0 4 1
A getLocale() 0 16 2
A getSetting() 0 11 1
A getTemplateId() 0 21 3
A getVariable() 0 4 1
A setVariables() 0 4 1
B importLocale() 0 28 3
A importSQL() 0 12 2
A insertDashboardWidget() 0 55 3
A getNextSequenceForModule() 0 17 2
B insertExtra() 0 24 2
A findModuleExtraId() 0 18 2
B insertMeta() 0 32 2
A getNextPageIdForLanguage() 0 9 1
A archiveAllRevisionsOfAPageForLanguage() 0 9 1
A getNextPageSequence() 0 9 1
A completeMetaRecord() 0 17 1
A getNewMetaId() 0 19 1
B completePageRevisionRecord() 0 35 5
B insertPage() 0 31 5
B completePageBlockRecords() 0 32 2
A installExample() 0 4 1
A makeSearchable() 0 8 1
B setActionRights() 0 25 2
A setModuleRights() 0 22 2
A getNextBackendNavigationSequence() 0 12 1
B setNavigation() 0 37 4
B setSetting() 0 37 3
A getAndCopyRandomImage() 0 20 1

How to fix   Complexity   

Complex Class

Complex classes like ModuleInstaller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModuleInstaller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Backend\Core\Installer;
4
5
/*
6
 * This file is part of Fork CMS.
7
 *
8
 * For the full copyright and license information, please view the license
9
 * file that was distributed with this source code.
10
 */
11
12
use Backend\Core\Engine\Model;
13
use Backend\Modules\Locale\Engine\Model as BackendLocaleModel;
14
use Common\ModuleExtraType;
15
use Common\Uri as CommonUri;
16
use SpoonDatabase;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Filesystem\Filesystem;
20
use Symfony\Component\Finder\Finder;
21
22
/**
23
 * The base-class for the installer
24
 */
25
abstract class ModuleInstaller
26
{
27
    /**
28
     * Database connection instance
29
     *
30
     * @var SpoonDatabase
31
     */
32
    private $database;
33
34
    /**
35
     * The module name.
36
     *
37
     * @var string
38
     */
39
    private $module;
40
41
    /**
42
     * The default extras that have to be added to every page.
43
     *
44
     * @var array
45
     */
46
    private $defaultExtras = [];
47
48
    /**
49
     * The frontend language(s)
50
     *
51
     * @var array
52
     */
53
    private $languages = [];
54
55
    /**
56
     * The backend language(s)
57
     *
58
     * @var array
59
     */
60
    private $interfaceLanguages = [];
61
62
    /**
63
     * Cached modules
64
     *
65
     * @var array
66
     */
67
    private static $modules = [];
68
69
    /**
70
     * The variables passed by the installer
71
     *
72
     * @var array
73
     */
74
    private $variables = [];
75
76
    /**
77
     * Should example data be installed.
78
     *
79
     * @var bool
80
     */
81
    private $example;
82
83
    /**
84
     * @var \Symfony\Component\Console\Input\InputInterface|null
85
     */
86
    private $input;
87
88 1
    /**
89
     * @var \Symfony\Component\Console\Output\OutputInterface|null
90
     */
91
    private $output;
92
93
    /**
94
     * @var array
95 1
     */
96 1
    protected $promptVariables = [];
97 1
98 1
    /**
99 1
     * @param SpoonDatabase $database The database-connection.
100 1
     * @param array $languages The selected frontend languages.
101
     * @param array $interfaceLanguages The selected backend languages.
102
     * @param bool $example Should example data be installed.
103
     * @param array $variables The passed variables.
104
     */
105
    public function __construct(
106
        SpoonDatabase $database,
107
        array $languages,
108
        array $interfaceLanguages,
109
        bool $example = false,
110
        array $variables = []
111
    ) {
112
        $this->database = $database;
113
        $this->languages = $languages;
114
        $this->interfaceLanguages = $interfaceLanguages;
115
        $this->example = $example;
116
        $this->variables = $variables;
117
    }
118
119 1
    /**
120
     * @return null|\Symfony\Component\Console\Input\InputInterface
121 1
     */
122
    public function getInput(): ?InputInterface
123
    {
124 1
        return $this->input;
125
    }
126
127 1
    /**
128 1
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
129
     */
130
    public function setInput($input): void
131
    {
132 1
        $this->input = $input;
133
    }
134 1
135
    /**
136
     * @return null|\Symfony\Component\Console\Output\OutputInterface
137
     */
138
    public function getOutput(): ?OutputInterface
139
    {
140
        return $this->output;
141
    }
142
143
    /**
144
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
145
     */
146
    public function setOutput($output): void
147
    {
148
        $this->output = $output;
149 1
    }
150
151
    /**
152 1
     * @return array
153
     */
154
    public function getPromptVariables(): array
155 1
    {
156
        return $this->promptVariables;
157 1
    }
158
159
    /**
160
     * Adds a default extra to the stack of extras
161 1
     *
162
     * @param int $extraId The extra id to add to every page.
163
     * @param string $position The position to put the default extra.
164
     */
165
    protected function addDefaultExtra(int $extraId, string $position): void
166 1
    {
167
        $this->defaultExtras[] = ['id' => $extraId, 'position' => $position];
168
    }
169
170
    /**
171 1
     * Inserts a new module.
172
     * The getModule method becomes available after using addModule and returns $module parameter.
173 1
     *
174
     * @param string $module The name of the module.
175
     */
176 1
    protected function addModule(string $module): void
177 1
    {
178
        $this->module = (string) $module;
179
180 1
        // module does not yet exists
181
        if (!(bool) $this->getDatabase()->getVar('SELECT 1 FROM modules WHERE name = ? LIMIT 1', $this->module)) {
182
            // build item
183
            $item = [
184
                'name' => $this->module,
185 1
                'installed_on' => gmdate('Y-m-d H:i:s'),
186 1
            ];
187 1
188
            // insert module
189
            $this->getDatabase()->insert('modules', $item);
190
191 1
            return;
192
        }
193
194
        // activate and update description
195
        $this->getDatabase()->update('modules', ['installed_on' => gmdate('Y-m-d H:i:s')], 'name = ?', $this->module);
196
    }
197
198
    /**
199
     * Add a search index
200
     *
201
     * @param string $module The module wherein will be searched.
202
     * @param int $otherId The id of the record.
203
     * @param array $fields A key/value pair of fields to index.
204
     * @param string $language The frontend language for this entry.
205
     */
206 1
    protected function addSearchIndex(string $module, int $otherId, array $fields, string $language): void
207
    {
208 1
        // get database
209
        $database = $this->getDatabase();
210
211
        // validate cache
212
        if (empty(self::$modules)) {
213
            // get all modules
214
            self::$modules = (array) $database->getColumn('SELECT m.name FROM modules AS m');
215
        }
216 1
217
        // module exists?
218 1
        if (!in_array('Search', self::$modules)) {
219
            return;
220
        }
221
222
        // no fields?
223
        if (empty($fields)) {
224
            return;
225
        }
226 1
227
        // insert search index
228 1
        foreach ($fields as $field => $value) {
229
            // reformat value
230
            $value = strip_tags((string) $value);
231
232
            // insert in database
233
            $database->execute(
234
                'INSERT INTO search_index (module, other_id, language, field, value, active)
235
                 VALUES (?, ?, ?, ?, ?, ?)
236 1
                 ON DUPLICATE KEY UPDATE value = ?, active = ?',
237
                [(string) $module, (int) $otherId, (string) $language, (string) $field, $value, true, $value, true]
238
            );
239
        }
240 1
241 1
        // invalidate the cache for search
242
        $finder = new Finder();
243
        $filesystem = new Filesystem();
244
        foreach ($finder->files()->in(FRONTEND_CACHE_PATH . '/Search/') as $file) {
245 1
            /** @var $file \SplFileInfo */
246
            $filesystem->remove($file->getRealPath());
247 1
        }
248 1
    }
249
250
    /**
251
     * Method that will be overridden by the specific installers
252
     */
253
    protected function execute(): void
254
    {
255
        // just a placeholder
256
    }
257 1
258
    /**
259 1
     * Get the database-handle
260
     *
261
     * @return SpoonDatabase
262
     */
263
    protected function getDatabase(): SpoonDatabase
264
    {
265
        return $this->database;
266
    }
267 1
268
    /**
269 1
     * Get the module name
270
     *
271
     * @return string
272
     */
273
    protected function getModule(): string
274
    {
275
        return $this->module;
276
    }
277
278
    /**
279
     * Set the module name
280
     *
281
     * @param string $module
282
     */
283 1
    protected function setModule(string $module): void
284
    {
285
        $this->module = $module;
286
    }
287
288
    /**
289
     * Get the default extras.
290 1
     *
291 1
     * @return array
292
     */
293
    public function getDefaultExtras(): array
294 1
    {
295
        return $this->defaultExtras;
296
    }
297 1
298
    /**
299
     * Get the default user
300
     *
301
     * @return int
302
     */
303
    protected function getDefaultUserID(): int
304
    {
305
        try {
306
            // fetch default user id
307
            return (int) $this->getDatabase()->getVar(
308 1
                'SELECT id
309
                 FROM users
310 1
                 WHERE is_god = ? AND active = ? AND deleted = ?
311 1
                 ORDER BY id ASC',
312 1
                [true, true, false]
313
            );
314
        } catch (\Exception $e) {
315 1
            return 1;
316
        }
317
    }
318
319
    /**
320
     * Get the selected cms interface languages
321
     *
322
     * @return array
323
     */
324
    protected function getInterfaceLanguages(): array
325
    {
326
        return $this->interfaceLanguages;
327
    }
328 1
329
    /**
330
     * Get the selected languages
331 1
     *
332 1
     * @return array
333
     */
334
    protected function getLanguages(): array
335
    {
336 1
        return $this->languages;
337
    }
338
339
    /**
340
     * Get a locale item.
341 1
     *
342 1
     * @param string $name
343
     * @param string $module
344
     * @param string $language The language abbreviation.
345
     * @param string $type The type of locale.
346 1
     * @param string $application
347
     *
348
     * @return string
349
     */
350
    protected function getLocale(
351
        string $name,
352
        string $module = 'Core',
353
        string $language = 'en',
354
        string $type = 'lbl',
355
        string $application = 'Backend'
356
    ): string {
357 1
        $translation = (string) $this->getDatabase()->getVar(
358
            'SELECT value
359 1
             FROM locale
360
             WHERE name = ? AND module = ? AND language = ? AND type = ? AND application = ?',
361
            [$name, $module, $language, $type, $application]
362
        );
363
364
        return ($translation !== '') ? $translation : $name;
365
    }
366
367
    /**
368 1
     * Get a setting
369
     *
370
     * @param string $module The name of the module.
371 1
     * @param string $name The name of the setting.
372
     *
373
     * @return mixed
374 1
     */
375
    protected function getSetting(string $module, string $name)
376
    {
377
        return unserialize(
378
            $this->getDatabase()->getVar(
379 1
                'SELECT value
380
                 FROM modules_settings
381
                 WHERE module = ? AND name = ?',
382 1
                [$module, $name]
383
            )
384
        );
385
    }
386
387 1
    /**
388 1
     * Get the id of the requested template of the active theme.
389 1
     *
390 1
     * @param string $template
391 1
     * @param string $theme
392 1
     *
393 1
     * @return int
394
     */
395 1
    protected function getTemplateId(string $template, string $theme = null): int
396
    {
397
        // no theme set = default theme
398
        if ($theme === null) {
399
            $theme = $this->getSetting('Core', 'theme');
400
        }
401
402 1
        // if the theme is still null we should fallback to the core
403
        if ($theme === null) {
404
            $theme = 'Fork';
405 1
        }
406
407
        // return best matching template id
408 1
        return (int) $this->getDatabase()->getVar(
409
            'SELECT id FROM themes_templates
410
             WHERE theme = ?
411
             ORDER BY path LIKE ? DESC, id ASC
412 1
             LIMIT 1',
413 1
            [$theme, '%' . $template . '%']
414
        );
415 1
    }
416
417
    /**
418 1
     * Get a variable
419
     *
420
     * @param string $name
421 1
     *
422 1
     * @return mixed
423 1
     */
424
    public function getVariable(string $name)
425 1
    {
426 1
        return $this->variables[$name] ?? null;
427 1
    }
428
429
    /**
430
     * Set a variables
431 1
     *
432
     * @param array $variables
433 1
     */
434
    public function setVariables(array $variables): void
435
    {
436 1
        $this->variables = $variables;
437
    }
438
439 1
    /**
440
     * Imports the locale XML file
441
     *
442 1
     * @param string $filename The full path for the XML-file.
443 1
     * @param bool $overwriteConflicts Should we overwrite when there is a conflict?
444 1
     */
445 1
    protected function importLocale(string $filename, bool $overwriteConflicts = false): void
446 1
    {
447
        // load the file content and execute it
448
        $content = trim(file_get_contents($filename));
449
450
        // file actually has content
451 1
        if (empty($content)) {
452
            return;
453 1
        }
454
455
        // load xml
456 1
        $xml = @simplexml_load_string($content);
457
458
        // import if it's valid xml
459 1
        if ($xml === false) {
460
            return;
461
        }
462 1
463 1
        // import locale
464 1
        BackendLocaleModel::importXML(
465 1
            $xml,
466 1
            $overwriteConflicts,
467
            $this->getLanguages(),
468
            $this->getInterfaceLanguages(),
469 1
            $this->getDefaultUserID(),
470
            gmdate('Y-m-d H:i:s')
471 1
        );
472
    }
473
474 1
    /**
475 1
     * Imports the sql file
476 1
     *
477
     * @param string $filename The full path for the SQL-file.
478
     */
479
    protected function importSQL(string $filename): void
480 1
    {
481 1
        // load the file content and execute it
482
        $queries = trim(file_get_contents($filename));
483
484 1
        // file actually has content
485 1
        if (empty($queries)) {
486
            return;
487
        }
488
489
        $this->getDatabase()->execute($queries);
490
    }
491
492
    protected function insertDashboardWidget(string $module, string $widget): void
493
    {
494
        // get database
495
        $database = $this->getDatabase();
496
497
        // fetch current settings
498
        $groupSettings = (array) $database->getRecords(
499
            'SELECT * FROM groups_settings WHERE name = ?',
500
            ['dashboard_sequence']
501
        );
502 1
        $userSettings = (array) $database->getRecords(
503
            'SELECT * FROM users_settings WHERE name = ?',
504
            ['dashboard_sequence']
505
        );
506
507
        // loop group settings
508
        foreach ($groupSettings as $settings) {
509
            // unserialize data
510
            $settings['value'] = unserialize($settings['value']);
511 1
512 1
            // add new widget
513 1
            $settings['value'][$module][] = $widget;
514
515
            // re-serialize value
516 1
            $settings['value'] = serialize($settings['value']);
517 1
518 1
            // update in database
519 1
            $database->update(
520 1
                'groups_settings',
521 1
                $settings,
522 1
                'group_id = ? AND name = ?',
523 1
                [$settings['group_id'], $settings['name']]
524
            );
525
        }
526
527
        // loop user settings
528
        foreach ($userSettings as $settings) {
529
            // unserialize data
530
            $settings['value'] = unserialize($settings['value']);
531
532
            // add new widget
533
            $settings['value'][$module][] = $widget;
534
535 1
            // re-serialize value
536
            $settings['value'] = serialize($settings['value']);
537
538 1
            // update in database
539 1
            $database->update(
540
                'users_settings',
541 1
                $settings,
542 1
                'user_id = ? AND name = ?',
543
                [$settings['user_id'], $settings['name']]
544 1
            );
545
        }
546
    }
547 1
548 1
    private function getNextSequenceForModule(string $module): int
549
    {
550
        // set next sequence number for this module
551 1
        $sequence = (int) $this->getDatabase()->getVar(
552
            'SELECT MAX(sequence) + 1 FROM modules_extras WHERE module = ?',
553
            [$module]
554
        );
555
556
        // this is the first extra for this module: generate new 1000-series
557
        if ($sequence > 0) {
558
            return $sequence;
559
        }
560
561
        return (int) $this->getDatabase()->getVar(
562
            'SELECT CEILING(MAX(sequence) / 1000) * 1000 FROM modules_extras'
563
        );
564
    }
565
566
    /**
567
     * Insert an extra
568
     *
569
     * @param string $module The module for the extra.
570
     * @param ModuleExtraType $type The type, possible values are: homepage, widget, block.
571
     * @param string $label The label for the extra.
572 1
     * @param string|null $action The action.
573
     * @param array|null $data data, will be passed in the extra.
574
     * @param bool $hidden Is this extra hidden?
575
     * @param int|null $sequence The sequence for the extra.
576
     *
577
     * @return int
578
     */
579
    protected function insertExtra(
580
        string $module,
581
        ModuleExtraType $type,
582
        string $label,
583
        string $action = null,
584
        array $data = null,
585
        bool $hidden = false,
586 1
        int $sequence = null
587 1
    ): int {
588
        $extraId = $this->findModuleExtraId($module, $type, $label, $data);
589 1
        if ($extraId !== 0) {
590 1
            return $extraId;
591 1
        }
592 1
593 1
        return Model::insertExtra(
594 1
            $type,
595 1
            $module,
596 1
            $action,
597 1
            $label,
598 1
            $data,
599 1
            $hidden,
600
            $sequence ?? $this->getNextSequenceForModule($module)
601
        );
602
    }
603
604
    /**
605
     * @param string $module
606
     * @param ModuleExtraType $type
607
     * @param string $label
608
     * @param array|null $data
609
     *
610
     * @return int
611
     */
612 1
    private function findModuleExtraId(string $module, ModuleExtraType $type, string $label, array $data = null): int
613
    {
614 1
        // build query
615 1
        $query = 'SELECT id FROM modules_extras WHERE module = ? AND type = ? AND label = ?';
616 1
        $parameters = [$module, $type, $label];
617
618
        if ($data === null) {
619 1
            $query .= ' AND data IS NULL';
620
621
            return (int) $this->getDatabase()->getVar($query, $parameters);
622 1
        }
623
624 1
        $query .= ' AND data = ?';
625 1
        $parameters[] = serialize($data);
626 1
627 1
        // get id (if it already exists)
628 1
        return (int) $this->getDatabase()->getVar($query, $parameters);
629
    }
630 1
631
    /**
632 1
     * Insert a meta item
633
     *
634 1
     * @param string $keywords The keyword of the item.
635 1
     * @param string $description A description of the item.
636 1
     * @param string $title The page title for the item.
637
     * @param string $url The unique URL.
638
     * @param bool $keywordsOverwrite Should the keywords be overwritten?
639 1
     * @param bool $descriptionOverwrite Should the descriptions be overwritten?
640
     * @param bool $titleOverwrite Should the page title be overwritten?
641
     * @param bool $urlOverwrite Should the URL be overwritten?
642
     * @param string $custom Any custom meta-data.
643
     * @param string $seoFollow Any custom meta-data.
644
     * @param string $seoIndex Any custom meta-data.
645
     * @param array $data Any custom meta-data.
646
     *
647
     * @return int
648
     */
649
    protected function insertMeta(
650 1
        string $keywords,
651
        string $description,
652 1
        string $title,
653 1
        string $url,
654 1
        bool $keywordsOverwrite = false,
655 1
        bool $descriptionOverwrite = false,
656 1
        bool $titleOverwrite = false,
657 1
        bool $urlOverwrite = false,
658 1
        string $custom = null,
659 1
        string $seoFollow = null,
660 1
        string $seoIndex = null,
661 1
        array $data = null
662 1
    ): int {
663 1
        return (int) $this->getDatabase()->insert(
664
            'meta',
665 1
            [
666
                'keywords' => $keywords,
667
                'keywords_overwrite' => $keywordsOverwrite,
668 1
                'description' => $description,
669
                'description_overwrite' => $descriptionOverwrite,
670 1
                'title' => $title,
671
                'title_overwrite' => $titleOverwrite,
672 1
                'url' => CommonUri::getUrl($url),
673 1
                'url_overwrite' => $urlOverwrite,
674 1
                'custom' => $custom,
675 1
                'seo_follow' => $seoFollow,
676 1
                'seo_index' => $seoIndex,
677 1
                'data' => $data !== null ? serialize($data) : null,
678 1
            ]
679 1
        );
680 1
    }
681 1
682 1
    /**
683 1
     * Looks for the next page id, if it is the first page it will default to 1
684 1
     *
685
     * @param string $language
686
     *
687
     * @return int
688 1
     */
689
    private function getNextPageIdForLanguage(string $language): int
690 1
    {
691 1
        $maximumPageId = (int) $this->getDatabase()->getVar(
692 1
            'SELECT MAX(id) FROM pages WHERE language = ?',
693 1
            [$language]
694 1
        );
695 1
696 1
        return ++$maximumPageId;
697 1
    }
698 1
699 1
    private function archiveAllRevisionsOfAPageForLanguage(int $pageId, string $language): void
700 1
    {
701 1
        $this->getDatabase()->update(
702 1
            'pages',
703 1
            ['status' => 'archive'],
704 1
            'id = ? AND language = ?',
705 1
            [$pageId, $language]
706 1
        );
707 1
    }
708 1
709 1
    private function getNextPageSequence(string $language, int $parentId, string $type): int
710 1
    {
711
        $maximumPageSequence = (int) $this->getDatabase()->getVar(
712 1
            'SELECT MAX(sequence) FROM pages WHERE language = ? AND parent_id = ? AND type = ?',
713
            [$language, $parentId, $type]
714 1
        );
715 1
716
        return ++$maximumPageSequence;
717 1
    }
718 1
719
    /**
720
     * Add the missing data to the meta record
721 1
     *
722
     * @param array $meta
723
     * @param string $defaultValue
724
     *
725
     * @return array
726
     */
727
    private function completeMetaRecord(array $meta, string $defaultValue): array
728
    {
729
        $meta['keywords'] = $meta['keywords'] ?? $defaultValue;
730
        $meta['keywords_overwrite'] = $meta['keywords_overwrite'] ?? false;
731
        $meta['description'] = $meta['description'] ?? $defaultValue;
732
        $meta['description_overwrite'] = $meta['description_overwrite'] ?? false;
733
        $meta['title'] = $meta['title'] ?? $defaultValue;
734
        $meta['title_overwrite'] = $meta['title_overwrite'] ?? false;
735
        $meta['url'] = $meta['url'] ?? $defaultValue;
736 1
        $meta['url_overwrite'] = $meta['url_overwrite'] ?? false;
737
        $meta['custom'] = $meta['custom'] ?? null;
738
        $meta['seo_follow'] = $meta['seo_follow'] ?? null;
739 1
        $meta['seo_index'] = $meta['seo_index'] ?? null;
740
        $meta['data'] = $meta['data'] ?? null;
741
742 1
        return $meta;
743
    }
744
745
    private function getNewMetaId(array $meta, string $defaultValue): int
746 1
    {
747 1
        $meta = $this->completeMetaRecord($meta, $defaultValue);
748
749
        return $this->insertMeta(
750 1
            $meta['keywords'],
751
            $meta['description'],
752
            $meta['title'],
753 1
            $meta['url'],
754
            $meta['keywords_overwrite'],
755 1
            $meta['description_overwrite'],
756 1
            $meta['title_overwrite'],
757
            $meta['url_overwrite'],
758
            $meta['custom'],
759 1
            $meta['seo_follow'],
760 1
            $meta['seo_index'],
761 1
            $meta['data']
762
        );
763
    }
764
765 1
    private function completePageRevisionRecord(array $revision, array $meta = []): array
766
    {
767
        $revision['id'] = $revision['id'] ?? $this->getNextPageIdForLanguage($revision['language']);
768 1
        $revision['user_id'] = $revision['user_id'] ?? $this->getDefaultUserID();
769
        $revision['template_id'] = $revision['template_id'] ?? $this->getTemplateId('Default');
770
        $revision['type'] = $revision['type'] ?? 'page';
771 1
        $revision['parent_id'] = $revision['parent_id'] ?? ($revision['type'] === 'page' ? 1 : 0);
772
        $revision['navigation_title'] = $revision['navigation_title'] ?? $revision['title'];
773 1
        $revision['navigation_title_overwrite'] = $revision['navigation_title_overwrite'] ?? false;
774 1
        $revision['hidden'] = $revision['hidden'] ?? false;
775 1
        $revision['status'] = $revision['status'] ?? 'active';
776 1
        $revision['publish_on'] = $revision['publish_on'] ?? gmdate('Y-m-d H:i:s');
777 1
        $revision['created_on'] = $revision['created_on'] ?? gmdate('Y-m-d H:i:s');
778 1
        $revision['edited_on'] = $revision['edited_on'] ?? gmdate('Y-m-d H:i:s');
779 1
        $revision['data'] = $revision['data'] ?? null;
780 1
        $revision['allow_move'] = $revision['allow_move'] ?? true;
781 1
        $revision['allow_children'] = $revision['allow_children'] ?? true;
782 1
        $revision['allow_edit'] = $revision['allow_edit'] ?? true;
783 1
        $revision['allow_delete'] = $revision['allow_delete'] ?? true;
784
        $revision['sequence'] = $revision['sequence'] ?? $this->getNextPageSequence(
785
            $revision['language'],
786 1
            $revision['parent_id'],
787 1
            $revision['type']
788
        );
789
        $revision['meta_id'] = $revision['meta_id'] ?? $this->getNewMetaId($meta, $revision['title']);
790
791
        if (!isset($revision['data']['image']) && $this->installExample()) {
792
            $revision['data']['image'] = $this->getAndCopyRandomImage();
793 1
        }
794
        if ($revision['data'] !== null) {
795 1
            $revision['data'] = serialize($revision['data']);
796 1
        }
797 1
798
        return $revision;
799
    }
800
801
    /**
802
     * Insert a page
803
     *
804
     * @param array $revision An array with the revision data.
805
     * @param array $meta The meta-data.
806 1
     * @param array[] $blocks The blocks.
807
     *
808 1
     * @throws \SpoonDatabaseException
809
     * @throws \SpoonException
810
     *
811
     * @return int
812
     */
813
    protected function insertPage(array $revision, array $meta = null, array ...$blocks): int
814
    {
815
        // build revision
816
        if (!isset($revision['language'])) {
817
            throw new \SpoonException('language is required for installing pages');
818 1
        }
819
        if (!isset($revision['title'])) {
820 1
            throw new \SpoonException('title is required for installing pages');
821 1
        }
822
        // deactivate previous page revisions
823 1
        if (isset($revision['id'])) {
824
            $this->archiveAllRevisionsOfAPageForLanguage($revision['id'], $revision['language']);
825 1
        }
826
827
        $revision = $this->completePageRevisionRecord($revision, (array) $meta);
828
829
        // insert page
830
        $revision['revision_id'] = $this->getDatabase()->insert('pages', $revision);
831
832
        if (empty($blocks)) {
833
            return $revision['id'];
834
        }
835 1
836
        $this->getDatabase()->insert(
837
            'pages_blocks',
838 1
            $this->completePageBlockRecords($blocks, $revision['revision_id'])
839 1
        );
840
841
        // return page id
842
        return $revision['id'];
843 1
    }
844
845
    private function completePageBlockRecords(array $blocks, int $defaultRevisionId): array
846 1
    {
847
        // array of positions and linked blocks (will be used to automatically set block sequence)
848
        $positions = [];
849
850 1
        return array_map(
851 1
            function (array $block) use (&$positions, $defaultRevisionId) {
852
                $block['position'] = $block['position'] ?? 'main';
853 1
                $positions[$block['position']][] = $block;
854 1
                $block['revision_id'] = $block['revision_id'] ?? $defaultRevisionId;
855 1
                $block['created_on'] = $block['created_on'] ?? gmdate('Y-m-d H:i:s');
856 1
                $block['edited_on'] = $block['edited_on'] ?? gmdate('Y-m-d H:i:s');
857
                $block['extra_id'] = $block['extra_id'] ?? null;
858
                $block['visible'] = $block['visible'] ?? true;
859 1
                $block['sequence'] = $block['sequence'] ?? count($positions[$block['position']]) - 1;
860
                $block['html'] = $block['html'] ?? '';
861
862
                // get the html from the template file if it is defined
863
                if (!empty($block['html'])) {
864
                    $block['html'] = file_get_contents($block['html']);
865
                }
866
867 1
                // sort array by its keys, so the array is always the same for SpoonDatabase::insert,
868
                // when you don't provide an array with arrays sorted in the same order, the fields get
869 1
                // mixed into different columns
870 1
                ksort($block);
871
872
                return $block;
873
            },
874 1
            $blocks
875
        );
876
    }
877 1
878
    /**
879
     * Should example data be installed
880
     *
881 1
     * @return bool
882 1
     */
883
    protected function installExample(): bool
884 1
    {
885 1
        return $this->example;
886
    }
887
888 1
    /**
889
     * Make a module searchable
890 1
     *
891
     * @param string $module The module to make searchable.
892
     * @param bool $searchable Enable/disable search for this module by default?
893 1
     * @param int $weight Set default search weight for this module.
894 1
     */
895
    protected function makeSearchable(string $module, bool $searchable = true, int $weight = 1): void
896
    {
897 1
        $this->getDatabase()->execute(
898
            'INSERT INTO search_modules (module, searchable, weight) VALUES (?, ?, ?)
899
             ON DUPLICATE KEY UPDATE searchable = ?, weight = ?',
900 1
            [$module, $searchable, $weight, $searchable, $weight]
901
        );
902
    }
903
904
    /**
905
     * Set the rights for an action
906
     *
907
     * @param int $groupId The group wherefore the rights will be set.
908
     * @param string $module The module wherein the action appears.
909
     * @param string $action The action wherefore the rights have to set.
910
     * @param int $level The level, default is 7 (max).
911
     */
912
    protected function setActionRights(int $groupId, string $module, string $action, int $level = 7): void
913
    {
914 1
        // check if the action already exists
915
        $actionRightAlreadyExist = (bool) $this->getDatabase()->getVar(
916
            'SELECT 1
917
             FROM groups_rights_actions
918
             WHERE group_id = ? AND module = ? AND action = ?
919
             LIMIT 1',
920
            [$groupId, $module, $action]
921
        );
922 1
923
        if ($actionRightAlreadyExist) {
924 1
            return;
925
        }
926
927 1
        $this->getDatabase()->insert(
928
            'groups_rights_actions',
929
            [
930 1
                'group_id' => $groupId,
931 1
                'module' => $module,
932
                'action' => $action,
933
                'level' => $level,
934
            ]
935 1
        );
936 1
    }
937
938
    /**
939
     * Sets the rights for a module
940 1
     *
941 1
     * @param int $groupId The group wherefore the rights will be set.
942
     * @param string $module The module too set the rights for.
943 1
     */
944 1
    protected function setModuleRights(int $groupId, string $module): void
945 1
    {
946 1
        $moduleRightAlreadyExist = (bool) $this->getDatabase()->getVar(
947 1
            'SELECT 1
948
             FROM groups_rights_modules
949
             WHERE group_id = ? AND module = ?
950
             LIMIT 1',
951
            [$groupId, $module]
952
        );
953
954
        if ($moduleRightAlreadyExist) {
955
            return;
956
        }
957
958
        $this->getDatabase()->insert(
959
            'groups_rights_modules',
960 1
            [
961
                'group_id' => $groupId,
962 1
                'module' => $module,
963
            ]
964 1
        );
965 1
    }
966 1
967
    private function getNextBackendNavigationSequence(int $parentId): int
968
    {
969 1
        // get maximum sequence for this parent
970
        $currentMaxBackendNavigationSequence = (int) $this->getDatabase()->getVar(
971
            'SELECT MAX(sequence)
972 1
             FROM backend_navigation
973
             WHERE parent_id = ?',
974
            [$parentId]
975
        );
976 1
977 1
        return ++$currentMaxBackendNavigationSequence;
978
    }
979
980
    /**
981 1
     * Set a new navigation item.
982
     *
983
     * @param int|null $parentId Id of the navigation item under we should add this.
984 1
     * @param string $label Label for the item.
985
     * @param string|null $url Url for the item. If omitted the first child is used.
986
     * @param array $selectedFor Set selected when these actions are active.
987
     * @param int $sequence Sequence to use for this item.
988 1
     *
989 1
     * @return int
990
     */
991 1
    protected function setNavigation(
992 1
        $parentId,
993 1
        string $label,
994
        string $url = null,
995
        array $selectedFor = null,
996 1
        int $sequence = null
997
    ): int {
998 1
        // if it is null we should cast it to int so we get a 0
999
        $parentId = (int) $parentId;
1000 1
1001
        $sequence = $sequence ?? $this->getNextBackendNavigationSequence($parentId);
1002 1
1003 1
        // get the id for this url
1004 1
        $id = (int) $this->getDatabase()->getVar(
1005
            'SELECT id
1006 1
             FROM backend_navigation
1007 1
             WHERE parent_id = ? AND label = ? AND url ' . ($url === null ? 'IS' : '=') . ' ?',
1008 1
            [$parentId, $label, $url]
1009
        );
1010 1
1011 1
        // already exists so return current id
1012 1
        if ($id !== 0) {
1013 1
            return $id;
1014
        }
1015
1016 1
        // doesn't exist yet, add it
1017
        return (int) $this->getDatabase()->insert(
1018
            'backend_navigation',
1019
            [
1020
                'parent_id' => $parentId,
1021
                'label' => $label,
1022
                'url' => $url,
1023
                'selected_for' => $selectedFor === null ? null : serialize($selectedFor),
1024
                'sequence' => $sequence,
1025
            ]
1026
        );
1027
    }
1028
1029
    /**
1030
     * Stores a module specific setting in the database.
1031
     *
1032
     * @param string $module The module wherefore the setting will be set.
1033
     * @param string $name The name of the setting.
1034
     * @param mixed $value The optional value.
1035
     * @param bool $overwrite Overwrite no matter what.
1036
     */
1037
    protected function setSetting(string $module, string $name, $value = null, bool $overwrite = false): void
1038
    {
1039
        $value = serialize($value);
1040
1041
        if ($overwrite) {
1042
            $this->getDatabase()->execute(
1043
                'INSERT INTO modules_settings (module, name, value)
1044
                 VALUES (?, ?, ?)
1045
                 ON DUPLICATE KEY UPDATE value = ?',
1046
                [$module, $name, $value, $value]
1047
            );
1048
1049
            return;
1050
        }
1051
1052
        // check if this setting already exists
1053
        $moduleSettingAlreadyExists = (bool) $this->getDatabase()->getVar(
1054
            'SELECT 1
1055
             FROM modules_settings
1056
             WHERE module = ? AND name = ?
1057
             LIMIT 1',
1058
            [$module, $name]
1059
        );
1060
1061
        if ($moduleSettingAlreadyExists) {
1062
            return;
1063
        }
1064
1065
        $this->getDatabase()->insert(
1066
            'modules_settings',
1067
            [
1068
                'module' => $module,
1069
                'name' => $name,
1070
                'value' => $value,
1071
            ]
1072
        );
1073
    }
1074
1075
    private function getAndCopyRandomImage(): string
1076
    {
1077
        $finder = new Finder();
1078
        $finder
1079
            ->files()
1080
            ->name('*.jpg')
1081
            ->in(__DIR__ . '/Data/images');
1082
1083
        $finder = iterator_to_array($finder);
1084
        $randomImage = $finder[array_rand($finder)];
1085
        $randomName = time() . '.jpg';
1086
1087
        $fileSystem = new Filesystem();
1088
        $fileSystem->copy(
1089
            $randomImage->getRealPath(),
1090
            __DIR__ . '/../../../Frontend/Files/Pages/images/source/' . $randomName
1091
        );
1092
1093
        return $randomName;
1094
    }
1095
}
1096