Passed
Push — dependabot/composer/doctrine/d... ( bc1a80...bcc5a8 )
by
unknown
47:19 queued 41:28
created

Model::copy()   D

Complexity

Conditions 15
Paths 164

Size

Total Lines 202
Code Lines 106

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 106
c 0
b 0
f 0
nc 164
nop 2
dl 0
loc 202
rs 4.3066

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Backend\Modules\Pages\Engine;
4
5
use Backend\Modules\ContentBlocks\Domain\ContentBlock\Command\CopyContentBlocksToOtherLocale;
6
use Backend\Modules\Location\Command\CopyLocationWidgetsToOtherLocale;
7
use SimpleBus\Message\Bus\MessageBus;
8
use InvalidArgumentException;
9
use SpoonFilter;
10
use Symfony\Component\Filesystem\Filesystem;
11
use Backend\Core\Engine\Authentication as BackendAuthentication;
12
use Backend\Core\Language\Language as BL;
13
use Backend\Core\Engine\Model as BackendModel;
14
use Backend\Core\Language\Locale;
15
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
16
use Backend\Modules\Search\Engine\Model as BackendSearchModel;
17
use Backend\Modules\Tags\Engine\Model as BackendTagsModel;
18
use ForkCMS\App\ForkController;
19
use Frontend\Core\Language\Language as FrontendLanguage;
20
21
/**
22
 * In this file we store all generic functions that we will be using in the PagesModule
23
 */
24
class Model
25
{
26
    const NO_PARENT_PAGE_ID = 0;
27
28
    const TYPE_OF_DROP_BEFORE = 'before';
29
    const TYPE_OF_DROP_AFTER = 'after';
30
    const TYPE_OF_DROP_INSIDE = 'inside';
31
    const POSSIBLE_TYPES_OF_DROP = [
32
        self::TYPE_OF_DROP_BEFORE,
33
        self::TYPE_OF_DROP_AFTER,
34
        self::TYPE_OF_DROP_INSIDE,
35
    ];
36
37
    const QUERY_BROWSE_RECENT =
38
        'SELECT i.id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
39
         FROM pages AS i
40
         WHERE i.status = ? AND i.language = ?
41
         ORDER BY i.edited_on DESC
42
         LIMIT ?';
43
44
    const QUERY_DATAGRID_BROWSE_DRAFTS =
45
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
46
         FROM pages AS i
47
         INNER JOIN
48
         (
49
             SELECT MAX(i.revision_id) AS revision_id
50
             FROM pages AS i
51
             WHERE i.status = ? AND i.user_id = ? AND i.language = ?
52
             GROUP BY i.id
53
         ) AS p
54
         WHERE i.revision_id = p.revision_id';
55
56
    const QUERY_BROWSE_REVISIONS =
57
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
58
         FROM pages AS i
59
         WHERE i.id = ? AND i.status = ? AND i.language = ?
60
         ORDER BY i.edited_on DESC';
61
62
    const QUERY_DATAGRID_BROWSE_SPECIFIC_DRAFTS =
63
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
64
         FROM pages AS i
65
         WHERE i.id = ? AND i.status = ? AND i.language = ?
66
         ORDER BY i.edited_on DESC';
67
68
    const QUERY_BROWSE_TEMPLATES =
69
        'SELECT i.id, i.label AS title
70
         FROM pages_templates AS i
71
         WHERE i.theme = ?
72
         ORDER BY i.label ASC';
73
74
    public static function getCacheBuilder(): CacheBuilder
75
    {
76
        static $cacheBuilder = null;
77
        if ($cacheBuilder === null) {
78
            $cacheBuilder = new CacheBuilder(BackendModel::get('database'), BackendModel::get('cache.pool'));
0 ignored issues
show
Bug introduced by
It seems like Backend\Core\Engine\Model::get('database') can also be of type null; however, parameter $database of Backend\Modules\Pages\En...eBuilder::__construct() does only seem to accept SpoonDatabase, 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

78
            $cacheBuilder = new CacheBuilder(/** @scrutinizer ignore-type */ BackendModel::get('database'), BackendModel::get('cache.pool'));
Loading history...
Bug introduced by
It seems like Backend\Core\Engine\Model::get('cache.pool') can also be of type null; however, parameter $cache of Backend\Modules\Pages\En...eBuilder::__construct() does only seem to accept Psr\Cache\CacheItemPoolInterface, 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

78
            $cacheBuilder = new CacheBuilder(BackendModel::get('database'), /** @scrutinizer ignore-type */ BackendModel::get('cache.pool'));
Loading history...
79
        }
80
81
        return $cacheBuilder;
82
    }
83
84
    public static function buildCache(string $language = null): void
85
    {
86
        $cacheBuilder = static::getCacheBuilder();
87
        $cacheBuilder->buildCache($language ?? BL::getWorkingLanguage());
88
    }
89
90
    public static function copy(string $fromLanguage, string $toLanguage): void
91
    {
92
        // get database
93
        $database = BackendModel::getContainer()->get('database');
94
95
        /** @var MessageBus $commanBus */
96
        $commandBus = BackendModel::get('command_bus');
97
98
        $toLocale = Locale::fromString($toLanguage);
99
        $fromLocale = Locale::fromString($fromLanguage);
100
101
        // copy contentBlocks and get copied contentBlockIds
102
        $copyContentBlocks = new CopyContentBlocksToOtherLocale($toLocale, $fromLocale);
103
        $commandBus->handle($copyContentBlocks);
104
        $contentBlockIds = $copyContentBlocks->extraIdMap;
105
106
        // define old block ids
107
        $contentBlockOldIds = array_keys($contentBlockIds);
108
        $locationWidgetOldIds = [];
109
        $locationWidgetIds = [];
110
        if (BackendModel::isModuleInstalled('Location')) {
111
            // copy location widgets and get copied widget ids
112
            $copyLocationWidgets = new CopyLocationWidgetsToOtherLocale($toLocale, $fromLocale);
113
            $commandBus->handle($copyLocationWidgets);
114
            $locationWidgetIds = $copyLocationWidgets->extraIdMap;
115
116
            // define old block ids
117
            $locationWidgetOldIds = array_keys($locationWidgetIds);
118
        }
119
120
        // get all old pages
121
        $ids = $database->getColumn(
122
            'SELECT id
123
             FROM pages AS i
124
             WHERE i.language = ? AND i.status = ?',
125
            [$toLanguage, 'active']
126
        );
127
128
        // any old pages
129
        if (!empty($ids)) {
130
            // delete existing pages
131
            foreach ($ids as $id) {
132
                // redefine
133
                $id = (int) $id;
134
135
                // get revision ids
136
                $revisionIDs = (array) $database->getColumn(
137
                    'SELECT i.revision_id
138
                     FROM pages AS i
139
                     WHERE i.id = ? AND i.language = ?',
140
                    [$id, $toLanguage]
141
                );
142
143
                // get meta ids
144
                $metaIDs = (array) $database->getColumn(
145
                    'SELECT i.meta_id
146
                     FROM pages AS i
147
                     WHERE i.id = ? AND i.language = ?',
148
                    [$id, $toLanguage]
149
                );
150
151
                // delete meta records
152
                if (!empty($metaIDs)) {
153
                    $database->delete('meta', 'id IN (' . implode(',', $metaIDs) . ')');
154
                }
155
156
                // delete blocks and their revisions
157
                if (!empty($revisionIDs)) {
158
                    $database->delete(
159
                        'pages_blocks',
160
                        'revision_id IN (' . implode(',', $revisionIDs) . ')'
161
                    );
162
                }
163
164
                // delete page and the revisions
165
                if (!empty($revisionIDs)) {
166
                    $database->delete('pages', 'revision_id IN (' . implode(',', $revisionIDs) . ')');
167
                }
168
            }
169
        }
170
171
        // delete search indexes
172
        $database->delete('search_index', 'module = ? AND language = ?', ['pages', $toLanguage]);
173
174
        // get all active pages
175
        $ids = BackendModel::getContainer()->get('database')->getColumn(
176
            'SELECT id
177
             FROM pages AS i
178
             WHERE i.language = ? AND i.status = ?',
179
            [$fromLanguage, 'active']
180
        );
181
182
        // loop
183
        foreach ($ids as $id) {
184
            // get data
185
            $sourceData = self::get($id, null, $fromLanguage);
186
187
            // get and build meta
188
            $meta = $database->getRecord(
189
                'SELECT *
190
                 FROM meta
191
                 WHERE id = ?',
192
                [$sourceData['meta_id']]
193
            );
194
195
            // remove id
196
            unset($meta['id']);
197
198
            // init page
199
            $page = [];
200
201
            // build page
202
            $page['id'] = $sourceData['id'];
203
            $page['user_id'] = BackendAuthentication::getUser()->getUserId();
204
            $page['parent_id'] = $sourceData['parent_id'];
205
            $page['template_id'] = $sourceData['template_id'];
206
            $page['meta_id'] = (int) $database->insert('meta', $meta);
207
            $page['language'] = $toLanguage;
208
            $page['type'] = $sourceData['type'];
209
            $page['title'] = $sourceData['title'];
210
            $page['navigation_title'] = $sourceData['navigation_title'];
211
            $page['navigation_title_overwrite'] = $sourceData['navigation_title_overwrite'];
212
            $page['hidden'] = $sourceData['hidden'];
213
            $page['status'] = 'active';
214
            $page['publish_on'] = BackendModel::getUTCDate();
215
            $page['created_on'] = BackendModel::getUTCDate();
216
            $page['edited_on'] = BackendModel::getUTCDate();
217
            $page['allow_move'] = $sourceData['allow_move'];
218
            $page['allow_children'] = $sourceData['allow_children'];
219
            $page['allow_edit'] = $sourceData['allow_edit'];
220
            $page['allow_delete'] = $sourceData['allow_delete'];
221
            $page['sequence'] = $sourceData['sequence'];
222
            $page['data'] = ($sourceData['data'] !== null) ? serialize($sourceData['data']) : null;
223
224
            // insert page, store the id, we need it when building the blocks
225
            $revisionId = self::insert($page);
226
227
            $blocks = [];
228
229
            // get the blocks
230
            $sourceBlocks = self::getBlocks($id, null, $fromLanguage);
231
232
            // loop blocks
233
            foreach ($sourceBlocks as $sourceBlock) {
234
                // build block
235
                $block = $sourceBlock;
236
                $block['revision_id'] = $revisionId;
237
                $block['created_on'] = BackendModel::getUTCDate();
238
                $block['edited_on'] = BackendModel::getUTCDate();
239
240
                // Overwrite the extra_id of the old content block with the id of the new one
241
                if (in_array($block['extra_id'], $contentBlockOldIds, true)) {
242
                    $block['extra_id'] = $contentBlockIds[$block['extra_id']];
243
                }
244
245
                // Overwrite the extra_id of the old location widget with the id of the new one
246
                if ((count($locationWidgetOldIds) > 0) && in_array($block['extra_id'], $locationWidgetOldIds, true)) {
247
                    $block['extra_id'] = $locationWidgetIds[$block['extra_id']];
248
                }
249
250
                // add block
251
                $blocks[] = $block;
252
            }
253
254
            // insert the blocks
255
            self::insertBlocks($blocks);
256
257
            $text = '';
258
259
            // build search-text
260
            foreach ($blocks as $block) {
261
                $text .= ' ' . $block['html'];
262
            }
263
264
            // add
265
            BackendSearchModel::saveIndex(
266
                'Pages',
267
                (int) $page['id'],
268
                ['title' => $page['title'], 'text' => $text],
269
                $toLanguage
270
            );
271
272
            // get tags
273
            $tags = BackendTagsModel::getTags('pages', $id, 'string', $fromLanguage);
274
275
            // save tags
276
            if ($tags != '') {
277
                $saveWorkingLanguage = BL::getWorkingLanguage();
278
279
                // If we don't set the working language to the target language,
280
                // BackendTagsModel::getUrl() will use the current working
281
                // language, possibly causing unnecessary '-2' suffixes in
282
                // tags.url
283
                BL::setWorkingLanguage($toLanguage);
284
285
                BackendTagsModel::saveTags($page['id'], $tags, 'pages', $toLanguage);
286
                BL::setWorkingLanguage($saveWorkingLanguage);
287
            }
288
        }
289
290
        // build cache
291
        self::buildCache($toLanguage);
292
    }
293
294
    /**
295
     * @deprecated Will become private since it is only used in this class
296
     */
297
    public static function createHtml(
298
        string $navigationType = 'page',
299
        int $depth = 0,
300
        int $parentId = BackendModel::HOME_PAGE_ID,
301
        string $html = ''
302
    ): string {
303
        $navigation = static::getCacheBuilder()->getNavigation(BL::getWorkingLanguage());
304
305
        // check if item exists
306
        if (isset($navigation[$navigationType][$depth][$parentId])) {
307
            // start html
308
            $html .= '<ul>' . "\n";
309
310
            // loop elements
311
            foreach ($navigation[$navigationType][$depth][$parentId] as $key => $aValue) {
312
                $html .= "\t<li>" . "\n";
313
                $html .= "\t\t" . '<a href="#">' . htmlspecialchars($aValue['navigation_title']) . '</a>' . "\n";
314
315
                // insert recursive here!
316
                if (isset($navigation[$navigationType][$depth + 1][$key])) {
317
                    $html .= self::createHtml(
318
                        $navigationType,
319
                        $depth + 1,
320
                        $parentId,
321
                        ''
322
                    );
323
                }
324
325
                // add html
326
                $html .= '</li>' . "\n";
327
            }
328
329
            // end html
330
            $html .= '</ul>' . "\n";
331
        }
332
333
        // return
334
        return $html;
335
    }
336
337
    /**
338
     * @param int $id The id of the page to delete.
339
     * @param string $language The language wherein the page will be deleted,
340
     *                           if not provided we will use the working language.
341
     * @param int $revisionId If specified the given revision will be deleted, used for deleting drafts.
342
     *
343
     * @return bool
344
     */
345
    public static function delete(int $id, string $language = null, int $revisionId = null): bool
346
    {
347
        $language = $language ?? BL::getWorkingLanguage();
348
349
        // get database
350
        $database = BackendModel::getContainer()->get('database');
351
352
        // get record
353
        $page = self::get($id, $revisionId, $language);
354
355
        // validate
356
        if (empty($page)) {
357
            return false;
358
        }
359
        if (!$page['allow_delete']) {
360
            return false;
361
        }
362
363
        // get revision ids
364
        $revisionIDs = (array) $database->getColumn(
365
            'SELECT i.revision_id
366
             FROM pages AS i
367
             WHERE i.id = ? AND i.language = ?',
368
            [$id, $language]
369
        );
370
371
        // get meta ids
372
        $metaIDs = (array) $database->getColumn(
373
            'SELECT i.meta_id
374
             FROM pages AS i
375
             WHERE i.id = ? AND i.language = ?',
376
            [$id, $language]
377
        );
378
379
        // delete meta records
380
        if (!empty($metaIDs)) {
381
            $database->delete('meta', 'id IN (' . implode(',', $metaIDs) . ')');
382
        }
383
384
        // delete blocks and their revisions
385
        if (!empty($revisionIDs)) {
386
            $database->delete('pages_blocks', 'revision_id IN (' . implode(',', $revisionIDs) . ')');
387
        }
388
389
        // delete page and the revisions
390
        if (!empty($revisionIDs)) {
391
            $database->delete('pages', 'revision_id IN (' . implode(',', $revisionIDs) . ')');
392
        }
393
394
        // delete tags
395
        BackendTagsModel::saveTags($id, '', 'Pages');
396
397
        // return
398
        return true;
399
    }
400
401
    public static function exists(int $pageId): bool
402
    {
403
        return (bool) BackendModel::getContainer()->get('database')->getVar(
404
            'SELECT 1
405
             FROM pages AS i
406
             WHERE i.id = ? AND i.language = ? AND i.status IN (?, ?)
407
             LIMIT 1',
408
            [$pageId, BL::getWorkingLanguage(), 'active', 'draft']
409
        );
410
    }
411
412
    /**
413
     * Get the data for a record
414
     *
415
     * @param int $pageId The Id of the page to fetch.
416
     * @param int $revisionId
417
     * @param string $language The language to use while fetching the page.
418
     *
419
     * @return mixed False if the record can't be found, otherwise an array with all data.
420
     */
421
    public static function get(int $pageId, int $revisionId = null, string $language = null)
422
    {
423
        // fetch revision if not specified
424
        if ($revisionId === null) {
425
            $revisionId = self::getLatestRevision($pageId, $language);
426
        }
427
428
        // redefine
429
        $language = $language ?? BL::getWorkingLanguage();
430
431
        // get page (active version)
432
        $return = (array) BackendModel::getContainer()->get('database')->getRecord(
433
            'SELECT i.*, UNIX_TIMESTAMP(i.publish_on) AS publish_on, UNIX_TIMESTAMP(i.created_on) AS created_on,
434
                UNIX_TIMESTAMP(i.edited_on) AS edited_on,
435
             IF(COUNT(e.id) > 0, 1, 0) AS has_extra,
436
             GROUP_CONCAT(b.extra_id) AS extra_ids
437
             FROM pages AS i
438
             LEFT OUTER JOIN pages_blocks AS b ON b.revision_id = i.revision_id AND b.extra_id IS NOT NULL
439
             LEFT OUTER JOIN modules_extras AS e ON e.id = b.extra_id AND e.type = ?
440
             WHERE i.id = ? AND i.revision_id = ? AND i.language = ?
441
             GROUP BY i.revision_id',
442
            ['block', $pageId, $revisionId, $language]
443
        );
444
445
        // no page?
446
        if (empty($return)) {
447
            return false;
448
        }
449
450
        $return['move_allowed'] = (bool) $return['allow_move'];
451
        $return['children_allowed'] = (bool) $return['allow_children'];
452
        $return['delete_allowed'] = (bool) $return['allow_delete'];
453
454
        if (self::isForbiddenToDelete($return['id'])) {
455
            $return['allow_delete'] = false;
456
        }
457
458
        if (self::isForbiddenToMove($return['id'])) {
459
            $return['allow_move'] = false;
460
        }
461
462
        if (self::isForbiddenToHaveChildren($return['id'])) {
463
            $return['allow_children'] = false;
464
        }
465
466
        // convert into bools for use in template engine
467
        $return['edit_allowed'] = (bool) $return['allow_edit'];
468
        $return['has_extra'] = (bool) $return['has_extra'];
469
470
        // unserialize data
471
        if ($return['data'] !== null) {
472
            $return['data'] = unserialize($return['data'], ['allowed_classes' => false]);
473
        }
474
475
        // return
476
        return $return;
477
    }
478
479
    public static function isForbiddenToDelete(int $pageId): bool
480
    {
481
        return in_array($pageId, [BackendModel::HOME_PAGE_ID, BackendModel::ERROR_PAGE_ID], true);
482
    }
483
484
    public static function isForbiddenToMove(int $pageId): bool
485
    {
486
        return in_array($pageId, [BackendModel::HOME_PAGE_ID, BackendModel::ERROR_PAGE_ID], true);
487
    }
488
489
    public static function isForbiddenToHaveChildren(int $pageId): bool
490
    {
491
        return $pageId === BackendModel::ERROR_PAGE_ID;
492
    }
493
494
    public static function getBlocks(int $pageId, int $revisionId = null, string $language = null): array
495
    {
496
        // fetch revision if not specified
497
        if ($revisionId === null) {
498
            $revisionId = self::getLatestRevision($pageId, $language);
499
        }
500
501
        // redefine
502
        $language = $language ?? BL::getWorkingLanguage();
503
504
        // get page (active version)
505
        return (array) BackendModel::getContainer()->get('database')->getRecords(
506
            'SELECT b.*, UNIX_TIMESTAMP(b.created_on) AS created_on, UNIX_TIMESTAMP(b.edited_on) AS edited_on
507
             FROM pages_blocks AS b
508
             INNER JOIN pages AS i ON b.revision_id = i.revision_id
509
                WHERE i.id = ? AND i.revision_id = ? AND i.language = ?
510
                ORDER BY b.sequence ASC',
511
            [$pageId, $revisionId, $language]
512
        );
513
    }
514
515
    public static function getByTag(int $tagId): array
516
    {
517
        // get the items
518
        $items = (array) BackendModel::getContainer()->get('database')->getRecords(
519
            'SELECT i.id AS url, i.title AS name, mt.module
520
             FROM modules_tags AS mt
521
             INNER JOIN tags AS t ON mt.tag_id = t.id
522
             INNER JOIN pages AS i ON mt.other_id = i.id
523
             WHERE mt.module = ? AND mt.tag_id = ? AND i.status = ? AND i.language = ?',
524
            ['pages', $tagId, 'active', BL::getWorkingLanguage()]
525
        );
526
527
        // loop items
528
        foreach ($items as &$row) {
529
            $row['url'] = BackendModel::createUrlForAction(
530
                'Edit',
531
                'Pages',
532
                null,
533
                ['id' => $row['url']]
534
            );
535
        }
536
537
        // return
538
        return $items;
539
    }
540
541
    /**
542
     * Get the first child for a given parent
543
     *
544
     * @param int $pageId The Id of the page to get the first child for.
545
     *
546
     * @return mixed
547
     */
548
    public static function getFirstChildId(int $pageId)
549
    {
550
        // get child
551
        $childId = (int) BackendModel::getContainer()->get('database')->getVar(
552
            'SELECT i.id
553
             FROM pages AS i
554
             WHERE i.parent_id = ? AND i.status = ? AND i.language = ?
555
             ORDER BY i.sequence ASC
556
             LIMIT 1',
557
            [$pageId, 'active', BL::getWorkingLanguage()]
558
        );
559
560
        // return
561
        if ($childId !== 0) {
562
            return $childId;
563
        }
564
565
        // fallback
566
        return false;
567
    }
568
569
    public static function getFullUrl(int $id): string
570
    {
571
        $keys = static::getCacheBuilder()->getKeys(BL::getWorkingLanguage());
572
        $hasMultiLanguages = BackendModel::getContainer()->getParameter('site.multilanguage');
573
574
        // available in generated file?
575
        if (isset($keys[$id])) {
576
            $url = $keys[$id];
577
        } elseif ($id == self::NO_PARENT_PAGE_ID) {
578
            // parent id 0 hasn't an url
579
            $url = '/';
580
581
            // multilanguages?
582
            if ($hasMultiLanguages) {
583
                $url = '/' . BL::getWorkingLanguage();
584
            }
585
586
            // return the unique URL!
587
            return $url;
588
        } else {
589
            // not available
590
            return false;
591
        }
592
593
        // if the is available in multiple languages we should add the current lang
594
        if ($hasMultiLanguages) {
595
            $url = '/' . BL::getWorkingLanguage() . '/' . $url;
596
        } else {
597
            // just prepend with slash
598
            $url = '/' . $url;
599
        }
600
601
        // return the unique URL!
602
        return urldecode($url);
603
    }
604
605
    public static function getLatestRevision(int $id, string $language = null): int
606
    {
607
        $language = $language ?? BL::getWorkingLanguage();
608
609
        return (int) BackendModel::getContainer()->get('database')->getVar(
610
            'SELECT revision_id
611
             FROM pages AS i
612
             WHERE i.id = ? AND i.language = ? AND i.status != ?',
613
            [$id, $language, 'archive']
614
        );
615
    }
616
617
    public static function getMaximumBlockId(): int
618
    {
619
        return (int) BackendModel::getContainer()->get('database')->getVar(
620
            'SELECT MAX(i.id) FROM pages_blocks AS i'
621
        );
622
    }
623
624
    public static function getMaximumPageId($language = null): int
625
    {
626
        $language = $language ?? BL::getWorkingLanguage();
627
628
        // get the maximum id
629
        $maximumMenuId = (int) BackendModel::getContainer()->get('database')->getVar(
630
            'SELECT MAX(i.id) FROM pages AS i WHERE i.language = ?',
631
            [$language]
632
        );
633
634
        // pages created by a user that isn't a god should have an id higher then 1000
635
        // with this hack we can easily find which pages are added by a user
636
        if ($maximumMenuId < 1000 && !BackendAuthentication::getUser()->isGod()) {
637
            return $maximumMenuId + 1000;
638
        }
639
640
        // fallback
641
        return $maximumMenuId;
642
    }
643
644
    public static function getMaximumSequence(int $parentId, string $language = null): int
645
    {
646
        $language = $language ?? BL::getWorkingLanguage();
647
648
        // get the maximum sequence inside a certain leaf
649
        return (int) BackendModel::getContainer()->get('database')->getVar(
650
            'SELECT MAX(i.sequence)
651
             FROM pages AS i
652
             WHERE i.language = ? AND i.parent_id = ?',
653
            [$language, $parentId]
654
        );
655
    }
656
657
    public static function getPagesForDropdown(string $language = null): array
658
    {
659
        $language = $language ?? BL::getWorkingLanguage();
660
        $titles = [];
661
        $sequences = [
662
            'pages' => [],
663
            'footer' => [],
664
        ];
665
        $keys = [];
666
        $pages = [];
667
        $pageTree = self::getTree([self::NO_PARENT_PAGE_ID], null, 1, $language);
668
        $homepageTitle = $pageTree[1][BackendModel::HOME_PAGE_ID]['title'] ?? SpoonFilter::ucfirst(BL::lbl('Home'));
669
670
        foreach ($pageTree as $pageTreePages) {
671
            foreach ((array) $pageTreePages as $pageID => $page) {
672
                $parentID = (int) $page['parent_id'];
673
674
                $keys[$pageID] = trim(($keys[$parentID] ?? '') . '/' . $page['url'], '/');
675
676
                $sequences[$page['type'] === 'footer' ? 'footer' : 'pages'][$keys[$pageID]] = $pageID;
677
678
                $parentTitle = str_replace([$homepageTitle . ' → ', $homepageTitle], '', $titles[$parentID] ?? '');
679
                $titles[$pageID] = htmlspecialchars(trim($parentTitle . ' → ' . $page['title'], ' → '));
680
            }
681
        }
682
683
        foreach ($sequences as $pageGroupSortList) {
684
            ksort($pageGroupSortList);
685
686
            foreach ($pageGroupSortList as $id) {
687
                if (isset($titles[$id])) {
688
                    $pages[$id] = $titles[$id];
689
                }
690
            }
691
        }
692
693
        return $pages;
694
    }
695
696
    private static function getSubTreeForDropdown(
697
        array $navigation,
698
        int $parentId,
699
        string $parentTitle,
700
        callable $attributesFunction
701
    ): array {
702
        if (!isset($navigation['page'][$parentId]) || empty($navigation['page'][$parentId])) {
703
            return [];
704
        }
705
        $tree = self::getEmptyTreeArray();
706
707
        foreach ($navigation['page'][$parentId] as $page) {
708
            $pageId = $page['page_id'];
709
            $pageTitle = htmlspecialchars($parentTitle . ' → ' . $page['navigation_title']);
710
            $tree['pages'][$pageId] = $pageTitle;
711
            $tree['attributes'][$pageId] = $attributesFunction($page);
712
713
            $tree = self::mergeTreeForDropdownArrays(
714
                $tree,
715
                self::getSubTreeForDropdown($navigation, $pageId, $pageTitle, $attributesFunction)
716
            );
717
        }
718
719
        return $tree;
720
    }
721
722
    private static function mergeTreeForDropdownArrays(array $tree, array $subTree, string $treeLabel = null): array
723
    {
724
        if (empty($subTree)) {
725
            return $tree;
726
        }
727
728
        $tree['attributes'] += $subTree['attributes'];
729
730
        if ($treeLabel === null) {
731
            $tree['pages'] += $subTree['pages'];
732
733
            return $tree;
734
        }
735
736
        $tree['pages'][$treeLabel] += $subTree['pages'];
737
738
        return $tree;
739
    }
740
741
    private static function getEmptyTreeArray(): array
742
    {
743
        return [
744
            'pages' => [],
745
            'attributes' => [],
746
        ];
747
    }
748
749
    private static function getAttributesFunctionForTreeName(
750
        string $treeName,
751
        string $treeLabel,
752
        int $currentPageId
753
    ): callable {
754
        return function (array $page) use ($treeName, $treeLabel, $currentPageId) {
755
            $isCurrentPage = $currentPageId === $page['page_id'];
756
757
            return [
758
                'data-tree-name' => $treeName,
759
                'data-tree-label' => $treeLabel,
760
                'data-allow-after' => (int) (!$isCurrentPage && $page['page_id'] !== BackendModel::HOME_PAGE_ID),
761
                'data-allow-inside' => (int) (!$isCurrentPage && $page['allow_children']),
762
                'data-allow-before' => (int) (!$isCurrentPage && $page['page_id'] !== BackendModel::HOME_PAGE_ID),
763
            ];
764
        };
765
    }
766
767
    private static function addMainPageToTreeForDropdown(
768
        array $tree,
769
        string $branchLabel,
770
        callable $attributesFunction,
771
        array $page,
772
        array $navigation
773
    ): array {
774
        $tree['pages'][$branchLabel][$page['page_id']] = SpoonFilter::htmlentities($page['navigation_title']);
775
        $tree['attributes'][$page['page_id']] = $attributesFunction($page);
776
777
        return self::mergeTreeForDropdownArrays(
778
            $tree,
779
            self::getSubTreeForDropdown(
780
                $navigation,
781
                $page['page_id'],
782
                $page['navigation_title'],
783
                $attributesFunction
784
            ),
785
            BL::lbl('MainNavigation')
786
        );
787
    }
788
789
    public static function getMoveTreeForDropdown(int $currentPageId, string $language = null): array
790
    {
791
        $navigation = static::getCacheBuilder()->getNavigation($language = $language ?? BL::getWorkingLanguage());
792
793
        $tree = self::addMainPageToTreeForDropdown(
794
            self::getEmptyTreeArray(),
795
            BL::lbl('MainNavigation'),
796
            self::getAttributesFunctionForTreeName('main', BL::lbl('MainNavigation'), $currentPageId),
797
            $navigation['page'][0][BackendModel::HOME_PAGE_ID],
798
            $navigation
799
        );
800
801
        $treeBranches = [];
802
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
803
            $treeBranches['meta'] = BL::lbl('Meta');
804
        }
805
        $treeBranches['footer'] = BL::lbl('Footer');
806
        $treeBranches['root'] = BL::lbl('Root');
807
808
        foreach ($treeBranches as $branchName => $branchLabel) {
809
            if (!isset($navigation[$branchName][0]) || !is_array($navigation[$branchName][0])) {
810
                continue;
811
            }
812
813
            foreach ($navigation[$branchName][0] as $page) {
814
                $tree = self::addMainPageToTreeForDropdown(
815
                    $tree,
816
                    $branchLabel,
817
                    self::getAttributesFunctionForTreeName($branchName, $branchLabel, $currentPageId),
818
                    $page,
819
                    $navigation
820
                );
821
            }
822
        }
823
824
        return $tree;
825
    }
826
827
    public static function getSubtree(array $navigation, int $parentId): string
828
    {
829
        $html = '';
830
831
        // any elements
832
        if (isset($navigation['page'][$parentId]) && !empty($navigation['page'][$parentId])) {
833
            // start
834
            $html .= '<ul>' . "\n";
835
836
            // loop pages
837
            foreach ($navigation['page'][$parentId] as $page) {
838
                // start
839
                $html .= '<li id="page-' . $page['page_id'] . '" rel="' . $page['tree_type'] . '">' . "\n";
840
841
                // insert link
842
                $html .= '    <a href="' . BackendModel::createUrlForAction(
843
                    'Edit',
844
                    null,
845
                    null,
846
                    ['id' => $page['page_id']]
847
                ) . '"><ins>&#160;</ins>' . htmlspecialchars($page['navigation_title']) . '</a>' . "\n";
848
849
                // get childs
850
                $html .= self::getSubtree($navigation, $page['page_id']);
851
852
                // end
853
                $html .= '</li>' . "\n";
854
            }
855
856
            // end
857
            $html .= '</ul>' . "\n";
858
        }
859
860
        // return
861
        return $html;
862
    }
863
864
    /**
865
     * Get all pages/level
866
     *
867
     * @param int[] $ids The parentIds.
868
     * @param array $data A holder for the generated data.
869
     * @param int $level The counter for the level.
870
     * @param string $language The language.
871
     *
872
     * @return array
873
     */
874
    public static function getTree(array $ids, array $data = null, int $level = 1, string $language = null): array
875
    {
876
        $language = $language ?? BL::getWorkingLanguage();
877
878
        // get data
879
        $data[$level] = (array) BackendModel::getContainer()->get('database')->getRecords(
880
            'SELECT
881
                 i.id, i.title, i.parent_id, i.navigation_title, i.type, i.hidden, i.data,
882
                m.url, m.data AS meta_data, m.seo_follow, m.seo_index, i.allow_children,
883
                IF(COUNT(e.id) > 0, 1, 0) AS has_extra,
884
                GROUP_CONCAT(b.extra_id) AS extra_ids,
885
                IF(COUNT(p.id), 1, 0) AS has_children
886
             FROM pages AS i
887
             INNER JOIN meta AS m ON i.meta_id = m.id
888
             LEFT OUTER JOIN pages_blocks AS b ON b.revision_id = i.revision_id
889
             LEFT OUTER JOIN modules_extras AS e ON e.id = b.extra_id AND e.type = ?
890
             LEFT OUTER JOIN pages AS p
891
                ON p.parent_id = i.id
892
                AND p.status = "active"
893
                AND p.hidden = "N"
894
                AND p.data NOT LIKE "%s:9:\"is_action\";b:1;%"
895
             AND p.language = i.language
896
             WHERE i.parent_id IN (' . implode(', ', $ids) . ')
897
                 AND i.status = ? AND i.language = ?
898
             GROUP BY i.revision_id
899
             ORDER BY i.sequence ASC',
900
            ['block', 'active', $language],
901
            'id'
902
        );
903
904
        // get the childIDs
905
        $childIds = array_keys($data[$level]);
906
907
        // build array
908
        if (!empty($data[$level])) {
909
            $data[$level] = array_map(
910
                function ($page) {
911
                    $page['has_extra'] = (bool) $page['has_extra'];
912
                    $page['has_children'] = (bool) $page['has_children'];
913
914
                    return $page;
915
                },
916
                $data[$level]
917
            );
918
919
            return self::getTree($childIds, $data, ++$level, $language);
920
        }
921
922
        unset($data[$level]);
923
924
        return $data;
925
    }
926
927
    public static function getTreeHTML(): string
928
    {
929
        $navigation = static::getCacheBuilder()->getNavigation(BL::getWorkingLanguage());
930
931
        // start HTML
932
        $html = '<h4>' . SpoonFilter::ucfirst(BL::lbl('MainNavigation')) . '</h4>' . "\n";
933
        $html .= '<div class="clearfix" data-tree="main">' . "\n";
934
        $html .= '    <ul>' . "\n";
935
        $html .= '        <li id="page-"' . BackendModel::HOME_PAGE_ID . ' rel="home">';
936
937
        // create homepage anchor from title
938
        $homePage = self::get(BackendModel::HOME_PAGE_ID);
939
        $html .= '            <a href="' . BackendModel::createUrlForAction(
940
            'Edit',
941
            null,
942
            null,
943
            ['id' => BackendModel::HOME_PAGE_ID]
944
        ) . '"><ins>&#160;</ins>' . $homePage['title'] . '</a>' . "\n";
945
946
        // add subpages
947
        $html .= self::getSubtree($navigation, BackendModel::HOME_PAGE_ID);
948
949
        // end
950
        $html .= '        </li>' . "\n";
951
        $html .= '    </ul>' . "\n";
952
        $html .= '</div>' . "\n";
953
954
        // only show meta if needed
955
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
956
            // meta pages
957
            $html .= '<h4>' . SpoonFilter::ucfirst(BL::lbl('Meta')) . '</h4>' . "\n";
958
            $html .= '<div class="clearfix" data-tree="meta">' . "\n";
959
            $html .= '    <ul>' . "\n";
960
961
            // are there any meta pages
962
            if (isset($navigation['meta'][0]) && !empty($navigation['meta'][0])) {
963
                // loop the items
964
                foreach ($navigation['meta'][0] as $page) {
965
                    // start
966
                    $html .= '        <li id="page-' . $page['page_id'] . '" rel="' . $page['tree_type'] . '">' . "\n";
967
968
                    // insert link
969
                    $html .= '            <a href="' . BackendModel::createUrlForAction(
970
                        'Edit',
971
                        null,
972
                        null,
973
                        ['id' => $page['page_id']]
974
                    ) . '"><ins>&#160;</ins>' . htmlspecialchars($page['navigation_title']) . '</a>' . "\n";
975
976
                    // insert subtree
977
                    $html .= self::getSubtree($navigation, $page['page_id']);
978
979
                    // end
980
                    $html .= '        </li>' . "\n";
981
                }
982
            }
983
984
            // end
985
            $html .= '    </ul>' . "\n";
986
            $html .= '</div>' . "\n";
987
        }
988
989
        // footer pages
990
        $html .= '<h4>' . SpoonFilter::ucfirst(BL::lbl('Footer')) . '</h4>' . "\n";
991
992
        // start
993
        $html .= '<div class="clearfix" data-tree="footer">' . "\n";
994
        $html .= '    <ul>' . "\n";
995
996
        // are there any footer pages
997
        if (isset($navigation['footer'][0]) && !empty($navigation['footer'][0])) {
998
            // loop the items
999
            foreach ($navigation['footer'][0] as $page) {
1000
                // start
1001
                $html .= '        <li id="page-' . $page['page_id'] . '" rel="' . $page['tree_type'] . '">' . "\n";
1002
1003
                // insert link
1004
                $html .= '            <a href="' . BackendModel::createUrlForAction(
1005
                    'Edit',
1006
                    null,
1007
                    null,
1008
                    ['id' => $page['page_id']]
1009
                ) . '"><ins>&#160;</ins>' . htmlspecialchars($page['navigation_title']) . '</a>' . "\n";
1010
1011
                // insert subtree
1012
                $html .= self::getSubtree($navigation, $page['page_id']);
1013
1014
                // end
1015
                $html .= '        </li>' . "\n";
1016
            }
1017
        }
1018
1019
        // end
1020
        $html .= '    </ul>' . "\n";
1021
        $html .= '</div>' . "\n";
1022
1023
        // are there any root pages
1024
        if (isset($navigation['root'][0]) && !empty($navigation['root'][0])) {
1025
            // meta pages
1026
            $html .= '<h4>' . SpoonFilter::ucfirst(BL::lbl('Root')) . '</h4>' . "\n";
1027
1028
            // start
1029
            $html .= '<div class="clearfix" data-tree="root">' . "\n";
1030
            $html .= '    <ul>' . "\n";
1031
1032
            // loop the items
1033
            foreach ($navigation['root'][0] as $page) {
1034
                // start
1035
                $html .= '        <li id="page-' . $page['page_id'] . '" rel="' . $page['tree_type'] . '">' . "\n";
1036
1037
                // insert link
1038
                $html .= '            <a href="' . BackendModel::createUrlForAction(
1039
                    'Edit',
1040
                    null,
1041
                    null,
1042
                    ['id' => $page['page_id']]
1043
                ) . '"><ins>&#160;</ins>' . htmlspecialchars($page['navigation_title']) . '</a>' . "\n";
1044
1045
                // insert subtree
1046
                $html .= self::getSubtree($navigation, $page['page_id']);
1047
1048
                // end
1049
                $html .= '        </li>' . "\n";
1050
            }
1051
1052
            // end
1053
            $html .= '    </ul>' . "\n";
1054
            $html .= '</div>' . "\n";
1055
        }
1056
1057
        // return
1058
        return $html;
1059
    }
1060
1061
    private static function pageIsChildOfParent(array $navigation, int $childId, int $parentId): bool
1062
    {
1063
        if (isset($navigation['page'][$parentId]) && !empty($navigation['page'][$parentId])) {
1064
            foreach ($navigation['page'][$parentId] as $page) {
1065
                if ($page['page_id'] === $childId) {
1066
                    return true;
1067
                }
1068
1069
                if (self::pageIsChildOfParent($navigation, $childId, $page['page_id'])) {
1070
                    return true;
1071
                }
1072
            }
1073
        }
1074
1075
        return false;
1076
    }
1077
1078
    public static function getTreeNameForPageId(int $pageId): ?string
1079
    {
1080
        $navigation = static::getCacheBuilder()->getNavigation(BL::getWorkingLanguage());
1081
1082
        if ($pageId === BackendModel::HOME_PAGE_ID || self::pageIsChildOfParent($navigation, $pageId, BackendModel::HOME_PAGE_ID)) {
1083
            return 'main';
1084
        }
1085
1086
        $treeNames = ['footer', 'root'];
1087
1088
        // only show meta if needed
1089
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
1090
            $treeNames[] = 'meta';
1091
        }
1092
1093
        foreach ($treeNames as $treeName) {
1094
            if (isset($navigation[$treeName][0]) && !empty($navigation[$treeName][0])) {
1095
                // loop the items
1096
                foreach ($navigation[$treeName][0] as $page) {
1097
                    if ($pageId === $page['page_id'] || self::pageIsChildOfParent($navigation, $pageId, $page['page_id'])) {
1098
                        return $treeName;
1099
                    }
1100
                }
1101
            }
1102
        }
1103
1104
        return null;
1105
    }
1106
1107
    public static function getTypes(): array
1108
    {
1109
        return [
1110
            'rich_text' => BL::lbl('Editor'),
1111
            'block' => BL::lbl('Module'),
1112
            'widget' => BL::lbl('Widget'),
1113
            'usertemplate' => BL::lbl('UserTemplate'),
1114
        ];
1115
    }
1116
1117
    public static function getUrl(string $url, int $id = null, int $parentId = null, bool $isAction = false): string
1118
    {
1119
        $parentIds = [$parentId ?? self::NO_PARENT_PAGE_ID];
1120
1121
        // 0, 1, 2, 3, 4 are all top levels, so we should place them on the same level
1122
        if ($parentId === self::NO_PARENT_PAGE_ID
1123
            || $parentId === BackendModel::HOME_PAGE_ID
1124
            || $parentId === 2
1125
            || $parentId === 3
1126
            || $parentId === 4
1127
        ) {
1128
            $parentIds = [
1129
                self::NO_PARENT_PAGE_ID,
1130
                BackendModel::HOME_PAGE_ID,
1131
                2,
1132
                3,
1133
                4,
1134
            ];
1135
        }
1136
1137
        // get database
1138
        $database = BackendModel::getContainer()->get('database');
1139
1140
        // no specific id
1141
        if ($id === null) {
1142
            // no items?
1143
            if ((bool) $database->getVar(
1144
                'SELECT 1
1145
                 FROM pages AS i
1146
                 INNER JOIN meta AS m ON i.meta_id = m.id
1147
                 WHERE i.parent_id IN(' . implode(',', $parentIds) . ') AND i.status = ? AND m.url = ?
1148
                    AND i.language = ?
1149
                 LIMIT 1',
1150
                ['active', $url, BL::getWorkingLanguage()]
1151
            )
1152
            ) {
1153
                // add a number
1154
                $url = BackendModel::addNumber($url);
1155
1156
                // recall this method, but with a new URL
1157
                return self::getUrl($url, null, $parentId, $isAction);
1158
            }
1159
        } else {
1160
            // one item should be ignored
1161
            // there are items so, call this method again.
1162
            if ((bool) $database->getVar(
1163
                'SELECT 1
1164
                 FROM pages AS i
1165
                 INNER JOIN meta AS m ON i.meta_id = m.id
1166
                 WHERE i.parent_id IN(' . implode(',', $parentIds) . ') AND i.status = ?
1167
                    AND m.url = ? AND i.id != ? AND i.language = ?
1168
                 LIMIT 1',
1169
                ['active', $url, $id, BL::getWorkingLanguage()]
1170
            )
1171
            ) {
1172
                // add a number
1173
                $url = BackendModel::addNumber($url);
1174
1175
                // recall this method, but with a new URL
1176
                return self::getUrl($url, $id, $parentId, $isAction);
1177
            }
1178
        }
1179
1180
        // get full URL
1181
        $fullUrl = self::getFullUrl($parentId) . '/' . $url;
0 ignored issues
show
Bug introduced by
It seems like $parentId can also be of type null; however, parameter $id of Backend\Modules\Pages\Engine\Model::getFullUrl() does only seem to accept integer, 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

1181
        $fullUrl = self::getFullUrl(/** @scrutinizer ignore-type */ $parentId) . '/' . $url;
Loading history...
1182
1183
        // get info about parent page
1184
        $parentPageInfo = self::get($parentId, null, BL::getWorkingLanguage());
0 ignored issues
show
Bug introduced by
It seems like $parentId can also be of type null; however, parameter $pageId of Backend\Modules\Pages\Engine\Model::get() does only seem to accept integer, 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

1184
        $parentPageInfo = self::get(/** @scrutinizer ignore-type */ $parentId, null, BL::getWorkingLanguage());
Loading history...
1185
1186
        // does the parent have extras?
1187
        if (!$isAction && is_array($parentPageInfo) && isset($parentPageInfo['has_extra']) && $parentPageInfo['has_extra']) {
1188
            // set locale
1189
            FrontendLanguage::setLocale(BL::getWorkingLanguage(), true);
1190
1191
            // get all on-site action
1192
            $actions = FrontendLanguage::getActions();
1193
1194
            // if the new URL conflicts with an action we should rebuild the URL
1195
            if (in_array($url, $actions)) {
1196
                // add a number
1197
                $url = BackendModel::addNumber($url);
1198
1199
                // recall this method, but with a new URL
1200
                return self::getUrl($url, $id, $parentId, $isAction);
1201
            }
1202
        }
1203
1204
        // check if folder exists
1205
        if (is_dir(PATH_WWW . '/' . $fullUrl) || is_file(PATH_WWW . '/' . $fullUrl)) {
1206
            // add a number
1207
            $url = BackendModel::addNumber($url);
1208
1209
            // recall this method, but with a new URL
1210
            return self::getUrl($url, $id, $parentId, $isAction);
1211
        }
1212
1213
        // check if it is an application
1214
        if (array_key_exists(trim($fullUrl, '/'), ForkController::getRoutes())) {
1215
            // add a number
1216
            $url = BackendModel::addNumber($url);
1217
1218
            // recall this method, but with a new URL
1219
            return self::getUrl($url, $id, $parentId, $isAction);
1220
        }
1221
1222
        // return the unique URL!
1223
        return $url;
1224
    }
1225
1226
    public static function insert(array $page): int
1227
    {
1228
        return (int) BackendModel::getContainer()->get('database')->insert('pages', $page);
1229
    }
1230
1231
    /**
1232
     * Insert multiple blocks at once
1233
     *
1234
     * @param array $blocks The blocks to insert.
1235
     */
1236
    public static function insertBlocks(array $blocks): void
1237
    {
1238
        if (empty($blocks)) {
1239
            return;
1240
        }
1241
1242
        // get database
1243
        $database = BackendModel::getContainer()->get('database');
1244
1245
        // loop blocks
1246
        foreach ($blocks as $key => $block) {
1247
            if ($block['extra_type'] === 'usertemplate') {
1248
                $blocks[$key]['extra_id'] = null;
1249
            }
1250
        }
1251
1252
        // insert blocks
1253
        $database->insert('pages_blocks', $blocks);
1254
    }
1255
1256
    public static function loadUserTemplates(): array
1257
    {
1258
        $themePath = FRONTEND_PATH . '/Themes/';
1259
        $themePath .= BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork');
1260
        $filePath = $themePath . '/Core/Layout/Templates/UserTemplates/Templates.json';
1261
1262
        $userTemplates = [];
1263
1264
        $fs = new Filesystem();
1265
        if ($fs->exists($filePath)) {
1266
            $userTemplates = json_decode(file_get_contents($filePath), true);
1267
1268
            foreach ($userTemplates as &$userTemplate) {
1269
                $userTemplate['file'] =
1270
                    '/src/Frontend/Themes/' .
1271
                    BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork') .
1272
                    '/Core/Layout/Templates/UserTemplates/' .
1273
                    $userTemplate['file'];
1274
            }
1275
        }
1276
1277
        return $userTemplates;
1278
    }
1279
1280
    /**
1281
     * Move a page
1282
     *
1283
     * @param int $pageId The id for the page that has to be moved.
1284
     * @param int $droppedOnPageId The id for the page where to page has been dropped on.
1285
     * @param string $typeOfDrop The type of drop, possible values are: before, after, inside.
1286
     * @param string $tree The tree the item is dropped on, possible values are: main, meta, footer, root.
1287
     * @param string $language The language to use, if not provided we will use the working language.
1288
     *
1289
     * @return bool
1290
     */
1291
    public static function move(
1292
        int $pageId,
1293
        int $droppedOnPageId,
1294
        string $typeOfDrop,
1295
        string $tree,
1296
        string $language = null
1297
    ): bool {
1298
        $typeOfDrop = SpoonFilter::getValue($typeOfDrop, self::POSSIBLE_TYPES_OF_DROP, self::TYPE_OF_DROP_INSIDE);
1299
        $tree = SpoonFilter::getValue($tree, ['main', 'meta', 'footer', 'root'], 'root');
1300
        $language = $language ?? BL::getWorkingLanguage();
1301
1302
        // When dropping on the main navigation it should be added as a child of the home page
1303
        if ($tree === 'main' && $droppedOnPageId === 0) {
1304
            $droppedOnPageId = BackendModel::HOME_PAGE_ID;
1305
            $typeOfDrop = self::TYPE_OF_DROP_INSIDE;
1306
        }
1307
1308
        // reset type of drop for special pages
1309
        if ($droppedOnPageId === BackendModel::HOME_PAGE_ID || $droppedOnPageId === self::NO_PARENT_PAGE_ID) {
1310
            $typeOfDrop = self::TYPE_OF_DROP_INSIDE;
1311
        }
1312
1313
        $page = self::get($pageId, null, $language);
1314
        $droppedOnPage = self::get(
1315
            ($droppedOnPageId === self::NO_PARENT_PAGE_ID ? BackendModel::HOME_PAGE_ID : $droppedOnPageId),
1316
            null,
1317
            $language
1318
        );
1319
1320
        if (empty($page) || empty($droppedOnPage)) {
1321
            return false;
1322
        }
1323
1324
        try {
1325
            $newParent = self::getNewParent($droppedOnPageId, $typeOfDrop, $droppedOnPage);
1326
        } catch (InvalidArgumentException $invalidArgumentException) {
1327
            // parent doesn't allow children
1328
            return false;
1329
        }
1330
1331
        self::recalculateSequenceAfterMove(
1332
            $typeOfDrop,
1333
            self::getNewType($droppedOnPageId, $tree, $newParent, $droppedOnPage),
1334
            $pageId,
1335
            $language,
1336
            $newParent,
1337
            $droppedOnPage['id']
1338
        );
1339
1340
        self::updateUrlAfterMove($pageId, $page, $newParent);
1341
1342
        return true;
1343
    }
1344
1345
    public static function update(array $page): int
1346
    {
1347
        // get database
1348
        $database = BackendModel::getContainer()->get('database');
1349
1350
        if (self::isForbiddenToDelete($page['id'])) {
1351
            $page['allow_delete'] = false;
1352
        }
1353
1354
        if (self::isForbiddenToMove($page['id'])) {
1355
            $page['allow_move'] = false;
1356
        }
1357
1358
        if (self::isForbiddenToHaveChildren($page['id'])) {
1359
            $page['allow_children'] = false;
1360
        }
1361
1362
        // update old revisions
1363
        if ($page['status'] != 'draft') {
1364
            $database->update(
1365
                'pages',
1366
                ['status' => 'archive'],
1367
                'id = ? AND language = ?',
1368
                [(int) $page['id'], $page['language']]
1369
            );
1370
        } else {
1371
            $database->delete(
1372
                'pages',
1373
                'id = ? AND user_id = ? AND status = ? AND language = ?',
1374
                [(int) $page['id'], BackendAuthentication::getUser()->getUserId(), 'draft', $page['language']]
1375
            );
1376
        }
1377
1378
        // insert
1379
        $page['revision_id'] = (int) $database->insert('pages', $page);
1380
1381
        // how many revisions should we keep
1382
        $rowsToKeep = (int) BackendModel::get('fork.settings')->get('Pages', 'max_num_revisions', 20);
1383
1384
        // get revision-ids for items to keep
1385
        $revisionIdsToKeep = (array) $database->getColumn(
1386
            'SELECT i.revision_id
1387
             FROM pages AS i
1388
             WHERE i.id = ? AND i.status = ?
1389
             ORDER BY i.edited_on DESC
1390
             LIMIT ?',
1391
            [(int) $page['id'], 'archive', $rowsToKeep]
1392
        );
1393
1394
        // delete other revisions
1395
        if (!empty($revisionIdsToKeep)) {
1396
            // because blocks are linked by revision we should get all revisions we want to delete
1397
            $revisionsToDelete = (array) $database->getColumn(
1398
                'SELECT i.revision_id
1399
                 FROM pages AS i
1400
                 WHERE i.id = ? AND i.status = ? AND i.revision_id NOT IN(' . implode(', ', $revisionIdsToKeep) . ')',
1401
                [(int) $page['id'], 'archive']
1402
            );
1403
1404
            // any revisions to delete
1405
            if (!empty($revisionsToDelete)) {
1406
                $database->delete('pages', 'revision_id IN(' . implode(', ', $revisionsToDelete) . ')');
1407
                $database->delete('pages_blocks', 'revision_id IN(' . implode(', ', $revisionsToDelete) . ')');
1408
            }
1409
        }
1410
1411
        // return the new revision id
1412
        return $page['revision_id'];
1413
    }
1414
1415
    /**
1416
     * @param array $page
1417
     */
1418
    public static function updateRevisionData(int $pageId, int $revisionId, array $data): void
1419
    {
1420
        // get database
1421
        $database = BackendModel::getContainer()->get('database');
1422
1423
        // serialize the data
1424
        $data['data'] = serialize($data['data']);
1425
1426
        $database->update(
1427
            'pages',
1428
            $data,
1429
            'id = ? AND revision_id = ?',
1430
            [$pageId, $revisionId]
1431
        );
1432
    }
1433
1434
    /**
1435
     * Switch templates for all existing pages
1436
     *
1437
     * @param int $oldTemplateId The id of the new template to replace.
1438
     * @param int $newTemplateId The id of the new template to use.
1439
     * @param bool $overwrite Overwrite all pages with default blocks.
1440
     */
1441
    public static function updatePagesTemplates(int $oldTemplateId, int $newTemplateId, bool $overwrite = false): void
1442
    {
1443
        // fetch new template data
1444
        $newTemplate = BackendExtensionsModel::getTemplate($newTemplateId);
1445
        $newTemplate['data'] = @unserialize($newTemplate['data'], ['allowed_classes' => false]);
1446
1447
        // fetch all pages
1448
        $pages = (array) BackendModel::getContainer()->get('database')->getRecords(
1449
            'SELECT *
1450
             FROM pages
1451
             WHERE template_id = ? AND status IN (?, ?)',
1452
            [$oldTemplateId, 'active', 'draft']
1453
        );
1454
1455
        // there is no active/draft page with the old template id
1456
        if (empty($pages)) {
1457
            return;
1458
        }
1459
1460
        // loop pages
1461
        foreach ($pages as $page) {
1462
            // fetch blocks
1463
            $blocksContent = self::getBlocks($page['id'], $page['revision_id'], $page['language']);
1464
1465
            // unset revision id
1466
            unset($page['revision_id']);
1467
1468
            // change template
1469
            $page['template_id'] = $newTemplateId;
1470
1471
            // save new page revision
1472
            $page['revision_id'] = self::update($page);
1473
1474
            // overwrite all blocks with current defaults
1475
            if ($overwrite) {
1476
                $blocksContent = [];
1477
1478
                // fetch default blocks for this page
1479
                $defaultBlocks = [];
1480
                if (isset($newTemplate['data']['default_extras_' . $page['language']])) {
1481
                    $defaultBlocks = $newTemplate['data']['default_extras_' . $page['language']];
1482
                } elseif (isset($newTemplate['data']['default_extras'])) {
1483
                    $defaultBlocks = $newTemplate['data']['default_extras'];
1484
                }
1485
1486
                // loop positions
1487
                foreach ($defaultBlocks as $position => $blocks) {
1488
                    // loop blocks
1489
                    foreach ($blocks as $extraId) {
1490
                        // add to the list
1491
                        $blocksContent[] = [
1492
                            'revision_id' => $page['revision_id'],
1493
                            'position' => $position,
1494
                            'extra_id' => $extraId,
1495
                            'extra_type' => 'rich_text',
1496
                            'html' => '',
1497
                            'created_on' => BackendModel::getUTCDate(),
1498
                            'edited_on' => BackendModel::getUTCDate(),
1499
                            'visible' => true,
1500
                            'sequence' => count($defaultBlocks[$position]) - 1,
1501
                        ];
1502
                    }
1503
                }
1504
            } else {
1505
                // don't overwrite blocks, just re-use existing
1506
                // set new page revision id
1507
                foreach ($blocksContent as &$block) {
1508
                    $block['revision_id'] = $page['revision_id'];
1509
                    $block['created_on'] = BackendModel::getUTCDate(null, $block['created_on']);
1510
                    $block['edited_on'] = BackendModel::getUTCDate(null, $block['edited_on']);
1511
                }
1512
            }
1513
1514
            // insert the blocks
1515
            self::insertBlocks($blocksContent);
1516
        }
1517
    }
1518
1519
    public static function getEncodedRedirectUrl(string $redirectUrl): string
1520
    {
1521
        preg_match('!(http[s]?)://(.*)!i', $redirectUrl, $matches);
1522
        $urlChunks = explode('/', $matches[2]);
1523
        if (!empty($urlChunks)) {
1524
            // skip domain name
1525
            $domain = array_shift($urlChunks);
1526
            foreach ($urlChunks as &$urlChunk) {
1527
                $urlChunk = rawurlencode($urlChunk);
1528
            }
1529
            unset($urlChunk);
1530
            $redirectUrl = $matches[1] . '://' . $domain . '/' . implode('/', $urlChunks);
1531
        }
1532
1533
        return $redirectUrl;
1534
    }
1535
1536
    private static function getNewParent(int $droppedOnPageId, string $typeOfDrop, array $droppedOnPage): int
1537
    {
1538
        if ($droppedOnPageId === self::NO_PARENT_PAGE_ID) {
1539
            return self::NO_PARENT_PAGE_ID;
1540
        }
1541
1542
        if ($typeOfDrop === self::TYPE_OF_DROP_INSIDE) {
1543
            // check if item allows children
1544
            if (!$droppedOnPage['allow_children']) {
1545
                throw new InvalidArgumentException('Parent page is not allowed to have child pages');
1546
            }
1547
1548
            return $droppedOnPage['id'];
1549
        }
1550
1551
        // if the item has to be moved before or after
1552
        return $droppedOnPage['parent_id'];
1553
    }
1554
1555
    private static function getNewType(int $droppedOnPageId, string $tree, int $newParent, array $droppedOnPage): string
1556
    {
1557
        if ($droppedOnPageId === self::NO_PARENT_PAGE_ID) {
1558
            if ($tree === 'footer') {
1559
                return 'footer';
1560
            }
1561
1562
            if ($tree === 'meta') {
1563
                return 'meta';
1564
            }
1565
1566
            return 'root';
1567
        }
1568
1569
        if ($newParent === self::NO_PARENT_PAGE_ID) {
1570
            return $droppedOnPage['type'];
1571
        }
1572
1573
        return 'page';
1574
    }
1575
1576
    private static function recalculateSequenceAfterMove(
1577
        string $typeOfDrop,
1578
        string $newType,
1579
        int $pageId,
1580
        string $language,
1581
        string $newParent,
1582
        int $droppedOnPageId
1583
    ): void {
1584
        $database = BackendModel::getContainer()->get('database');
1585
1586
        // calculate new sequence for items that should be moved inside
1587
        if ($typeOfDrop === self::TYPE_OF_DROP_INSIDE) {
1588
            $newSequence = (int) $database->getVar(
1589
                'SELECT MAX(i.sequence)
1590
                 FROM pages AS i
1591
                 WHERE i.id = ? AND i.language = ? AND i.status = ?',
1592
                [$newParent, $language, 'active']
1593
            ) + 1;
1594
1595
            $database->update(
1596
                'pages',
1597
                [
1598
                    'parent_id' => $newParent,
1599
                    'sequence' => $newSequence,
1600
                    'type' => $newType
1601
                ],
1602
                'id = ? AND language = ? AND status = ?',
1603
                [$pageId, $language, 'active']
1604
            );
1605
1606
            return;
1607
        }
1608
1609
        // calculate new sequence for items that should be moved before or after
1610
        $droppedOnPageSequence = (int) $database->getVar(
1611
            'SELECT i.sequence
1612
             FROM pages AS i
1613
             WHERE i.id = ? AND i.language = ? AND i.status = ?
1614
             LIMIT 1',
1615
            [$droppedOnPageId, $language, 'active']
1616
        );
1617
1618
        $newSequence = $droppedOnPageSequence + ($typeOfDrop === self::TYPE_OF_DROP_BEFORE ? -1 : 1);
1619
1620
        // increment all pages with a sequence that is higher than the new sequence;
1621
        $database->execute(
1622
            'UPDATE pages
1623
                 SET sequence = sequence + 1
1624
                 WHERE parent_id = ? AND language = ? AND sequence > ?',
1625
            [$newParent, $language, $newSequence]
1626
        );
1627
1628
        $database->update(
1629
            'pages',
1630
            [
1631
                'parent_id' => $newParent,
1632
                'sequence' => $newSequence,
1633
                'type' => $newType
1634
            ],
1635
            'id = ? AND language = ? AND status = ?',
1636
            [$pageId, $language, 'active']
1637
        );
1638
    }
1639
1640
    private static function updateUrlAfterMove(int $pageId, array $page, int $newParent): void
1641
    {
1642
        $database = BackendModel::getContainer()->get('database');
1643
1644
        $currentUrl = (string) $database->getVar(
1645
            'SELECT url
1646
             FROM meta AS m
1647
             WHERE m.id = ?',
1648
            [$page['meta_id']]
1649
        );
1650
1651
        $newUrl = self::getUrl(
1652
            $currentUrl,
1653
            $pageId,
1654
            $newParent,
1655
            isset($page['data']['is_action']) && $page['data']['is_action']
1656
        );
1657
1658
        $database->update('meta', ['url' => $newUrl], 'id = ?', [$page['meta_id']]);
1659
    }
1660
}
1661