Passed
Pull Request — restructure-core (#3014)
by jelmer
08:13
created

Model::getUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 4
b 0
f 0
nc 1
nop 4
dl 0
loc 6
ccs 0
cts 4
cp 0
crap 2
rs 10
1
<?php
2
3
namespace Backend\Modules\Pages\Engine;
4
5
use Backend\Core\Engine\Authentication as BackendAuthentication;
6
use Backend\Core\Engine\Model as BackendModel;
7
use Backend\Core\Language\Language as BL;
8
use Backend\Core\Language\Locale;
9
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
10
use Backend\Modules\Pages\Domain\ModuleExtra\ModuleExtraRepository;
11
use Backend\Modules\Pages\Domain\Page\Page;
12
use Backend\Modules\Pages\Domain\Page\PageRepository;
13
use Backend\Modules\Pages\Domain\Page\Status;
14
use Backend\Modules\Pages\Domain\Page\Type;
15
use Backend\Modules\Pages\Domain\PageBlock\PageBlock;
16
use Backend\Modules\Pages\Domain\PageBlock\PageBlockRepository;
17
use Backend\Modules\Pages\Domain\PageBlock\Type as PageBlockType;
18
use Backend\Modules\Tags\Engine\Model as BackendTagsModel;
19
use Common\Doctrine\Entity\Meta;
20
use Common\Doctrine\Repository\MetaRepository;
21
use Common\Locale as AbstractLocale;
22
use InvalidArgumentException;
23
use RuntimeException;
24
use Symfony\Component\Filesystem\Filesystem;
25
26
/**
27
 * In this file we store all generic functions that we will be using in the PagesModule
28
 */
29
class Model
30
{
31
    const TYPE_OF_DROP_BEFORE = 'before';
32
    const TYPE_OF_DROP_AFTER = 'after';
33
    const TYPE_OF_DROP_INSIDE = 'inside';
34
    const POSSIBLE_TYPES_OF_DROP = [
35
        self::TYPE_OF_DROP_BEFORE,
36
        self::TYPE_OF_DROP_AFTER,
37
        self::TYPE_OF_DROP_INSIDE,
38
    ];
39
40
    const QUERY_BROWSE_RECENT =
41
        'SELECT i.id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
42
         FROM PagesPage AS i
43
         WHERE i.status = ? AND i.locale = ?
44
         ORDER BY i.edited_on DESC
45
         LIMIT ?';
46
47
    const QUERY_DATAGRID_BROWSE_DRAFTS =
48
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
49
         FROM PagesPage AS i
50
         INNER JOIN
51
         (
52
             SELECT MAX(i.revision_id) AS revision_id
53
             FROM PagesPage AS i
54
             WHERE i.status = ? AND i.user_id = ? AND i.locale = ?
55
             GROUP BY i.id
56
         ) AS p
57
         WHERE i.revision_id = p.revision_id';
58
59
    public static function getCacheBuilder(): CacheBuilder
60
    {
61
        static $cacheBuilder = null;
62
        if ($cacheBuilder === null) {
63
            $cacheBuilder = new CacheBuilder(
64
                BackendModel::get('database'),
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

64
                /** @scrutinizer ignore-type */ BackendModel::get('database'),
Loading history...
65
                BackendModel::get('cache.pool'),
0 ignored issues
show
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

65
                /** @scrutinizer ignore-type */ BackendModel::get('cache.pool'),
Loading history...
66
                BackendModel::get(ModuleExtraRepository::class)
0 ignored issues
show
Bug introduced by
It seems like Backend\Core\Engine\Mode...ExtraRepository::class) can also be of type null; however, parameter $moduleExtraRepository of Backend\Modules\Pages\En...eBuilder::__construct() does only seem to accept Backend\Modules\Pages\Do...a\ModuleExtraRepository, 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

66
                /** @scrutinizer ignore-type */ BackendModel::get(ModuleExtraRepository::class)
Loading history...
67
            );
68
        }
69
70
        return $cacheBuilder;
71
    }
72
73
    public static function buildCache(AbstractLocale $locale = null): void
74
    {
75
        $cacheBuilder = static::getCacheBuilder();
76
        $cacheBuilder->buildCache($locale ?? Locale::workingLocale());
77
    }
78
79
    /**
80
     * @param int $id The id of the page to delete.
81
     * @param Locale $locale The locale wherein the page will be deleted,
82
     *                           if not provided we will use the working locale.
83
     * @param int $revisionId If specified the given revision will be deleted, used for deleting drafts.
84
     *
85
     * @return bool
86
     */
87
    public static function delete(int $id, Locale $locale = null, int $revisionId = null): bool
88
    {
89
        /** @var PageRepository $pageRepository */
90
        $pageRepository = BackendModel::get(PageRepository::class);
91
92
        /** @var MetaRepository $metaRepository */
93
        $metaRepository = BackendModel::get('fork.repository.meta');
94
95
        $locale = $locale ?? Locale::workingLocale();
96
97
        // get record
98
        $page = self::get($id, $revisionId, $locale);
99
100
        // validate
101
        if (empty($page)) {
102
            return false;
103
        }
104
        if (!$page['allow_delete']) {
105
            return false;
106
        }
107
108
        // get revision ids
109
        $pages = $pageRepository->findBy(['id' => $id, 'locale' => $locale]);
110
111
        $revisionIDs = array_map(
112
            function (Page $page) {
113
                return $page->getRevisionId();
114
            },
115
            $pages
116
        );
117
118
        // delete meta records
119
        foreach ($pages as $page) {
120
            $metaRepository->remove($page->getMeta());
121
        }
122
123
        // delete blocks and their revisions
124
        if (!empty($revisionIDs)) {
125
            /** @var PageBlockRepository $pageBlockRepository */
126
            $pageBlockRepository = BackendModel::get(PageBlockRepository::class);
127
            $pageBlockRepository->deleteByRevisionIds($revisionIDs);
128
        }
129
130
        // delete page and the revisions
131
        if (!empty($revisionIDs)) {
132
            $pageRepository->deleteByRevisionIds($revisionIDs);
133
        }
134
135
        // delete tags
136
        BackendTagsModel::saveTags($id, '', 'Pages');
137
138
        // return
139
        return true;
140
    }
141
142
    public static function exists(int $pageId): bool
143
    {
144
        /** @var PageRepository $pageRepository */
145
        $pageRepository = BackendModel::get(PageRepository::class);
146
        $page = $pageRepository->findOneBy(
147
            [
148
                'id' => $pageId,
149
                'locale' => Locale::workingLocale(),
150
                'status' => [Status::active(), Status::draft()],
151
            ]
152
        );
153
154
        return $page instanceof Page;
155
    }
156
157
    /**
158
     * Get the data for a record
159
     *
160
     * @param int $pageId The Id of the page to fetch.
161
     * @param int $revisionId
162
     * @param Locale $locale The locale to use while fetching the page.
163
     *
164
     * @return mixed False if the record can't be found, otherwise an array with all data.
165
     */
166
    public static function get(int $pageId, int $revisionId = null, Locale $locale = null)
167
    {
168
        // fetch revision if not specified
169
        if ($revisionId === null) {
170
            $revisionId = self::getLatestRevision($pageId, $locale);
171
        }
172
173
        // redefine
174
        $locale = $locale ?? Locale::workingLocale();
175
176
        /** @var PageRepository $pageRepository */
177
        $pageRepository = BackendModel::get(PageRepository::class);
178
        $page = $pageRepository->getOne($pageId, $revisionId, $locale);
179
180
        // no page?
181
        if ($page === null) {
182
            return false;
183
        }
184
185
        return $page;
186
    }
187
188
    public static function isForbiddenToDelete(int $pageId): bool
189
    {
190
        return in_array($pageId, [Page::HOME_PAGE_ID, Page::ERROR_PAGE_ID], true);
191
    }
192
193
    public static function isForbiddenToMove(int $pageId): bool
194
    {
195
        return in_array($pageId, [Page::HOME_PAGE_ID, Page::ERROR_PAGE_ID], true);
196
    }
197
198
    public static function isForbiddenToHaveChildren(int $pageId): bool
199
    {
200
        return $pageId === Page::ERROR_PAGE_ID;
201
    }
202
203
    public static function getBlocks(int $pageId, int $revisionId = null, Locale $locale = null): array
204
    {
205
        // fetch revision if not specified
206
        if ($revisionId === null) {
207
            $revisionId = self::getLatestRevision($pageId, $locale);
208
        }
209
210
        // redefine
211
        $locale = $locale ?? Locale::workingLocale();
212
213
        /** @var PageBlockRepository $pageBlockRepository */
214
        $pageBlockRepository = BackendModel::get(PageBlockRepository::class);
215
216
        return $pageBlockRepository->getBlocksForPage($pageId, $revisionId, $locale);
217
    }
218
219
    public static function getByTag(int $tagId): array
220
    {
221
        // get the items
222
        $items = (array) BackendModel::getContainer()->get('database')->getRecords(
223
            'SELECT i.id AS url, i.title AS name, mt.moduleName AS module
224
             FROM TagsModuleTag AS mt
225
             INNER JOIN TagsTag AS t ON mt.tag_id = t.id AND mt.moduleName = ? AND mt.tag_id = ?
226
             INNER JOIN PagesPage AS i ON mt.moduleId = i.id AND i.status = ?',
227
            ['pages', $tagId, 'active']
228
        );
229
230
        // loop items
231
        foreach ($items as &$row) {
232
            $row['url'] = BackendModel::createUrlForAction(
233
                'Edit',
234
                'Pages',
235
                null,
236
                ['id' => $row['url']]
237
            );
238
        }
239
240
        // return
241
        return $items;
242
    }
243
244
    /**
245
     * Get the id first child for a given parent
246
     */
247
    public static function getFirstChildId(int $parentId): ?int
248
    {
249
        /** @var PageRepository $pageRepository */
250
        $pageRepository = BackendModel::get(PageRepository::class);
251
        $page = $pageRepository->getFirstChild($parentId, Status::active(), Locale::workingLocale());
252
253
        if ($page instanceof Page) {
254
            return $page->getId();
255
        }
256
257
        return null;
258
    }
259
260
    public static function getFullUrl(int $id): string
261
    {
262
        $keys = static::getCacheBuilder()->getKeys(Locale::workingLocale());
263
        $hasMultiLanguages = BackendModel::getContainer()->getParameter('site.multilanguage');
264
265
        // available in generated file?
266
        if (isset($keys[$id])) {
267
            $url = $keys[$id];
268
        } elseif ($id == Page::NO_PARENT_PAGE_ID) {
269
            // parent id 0 hasn't an url
270
            $url = '/';
271
272
            // multilanguages?
273
            if ($hasMultiLanguages) {
274
                $url = '/' . BL::getWorkingLanguage();
275
            }
276
277
            // return the unique URL!
278
            return $url;
279
        } else {
280
            // not available
281
            return false;
282
        }
283
284
        // if the is available in multiple languages we should add the current lang
285
        if ($hasMultiLanguages) {
286
            $url = '/' . BL::getWorkingLanguage() . '/' . $url;
287
        } else {
288
            // just prepend with slash
289
            $url = '/' . $url;
290
        }
291
292
        // return the unique URL!
293
        return urldecode($url);
294
    }
295
296
    public static function getLatestRevision(int $id, Locale $locale = null): int
297
    {
298
        $locale = $locale ?? Locale::workingLocale();
299
300
        /** @var PageRepository $pageRepository */
301
        $pageRepository = BackendModel::get(PageRepository::class);
302
303
        return (int) $pageRepository->getLatestVersion($id, $locale);
304
    }
305
306
    public static function getMaximumPageId(Locale $locale = null): int
307
    {
308
        /** @var PageRepository $pageRepository */
309
        $pageRepository = BackendModel::get(PageRepository::class);
310
311
        return $pageRepository->getMaximumPageId(
312
            $locale ?? Locale::workingLocale(),
313
            BackendAuthentication::getUser()->isGod()
314
        );
315
    }
316
317
    public static function getMaximumSequence(int $parentId, Locale $locale = null): int
318
    {
319
        $locale = $locale ?? Locale::workingLocale();
320
321
        /** @var PageRepository $pageRepository */
322
        $pageRepository = BackendModel::get(PageRepository::class);
323
324
        // get the maximum sequence inside a certain leaf
325
        return $pageRepository->getMaximumSequence($parentId, $locale);
326
    }
327
328
    public static function getPagesForDropdown(Locale $locale = null): array
329
    {
330
        $locale = $locale ?? Locale::workingLocale();
331
        $titles = [];
332
        $sequences = [
333
            'pages' => [],
334
            'footer' => [],
335
        ];
336
        $keys = [];
337
        $pages = [];
338
        $pageTree = self::getTree([Page::NO_PARENT_PAGE_ID], null, 1, $locale);
339
        $homepageTitle = $pageTree[1][Page::HOME_PAGE_ID]['title'] ?? \SpoonFilter::ucfirst(BL::lbl('Home'));
340
341
        foreach ($pageTree as $pageTreePages) {
342
            foreach ((array) $pageTreePages as $pageID => $page) {
343
                $parentID = (int) $page['parent_id'];
344
345
                $keys[$pageID] = trim(($keys[$parentID] ?? '') . '/' . $page['url'], '/');
346
347
                $sequences[$page['type'] === 'footer' ? 'footer' : 'pages'][$keys[$pageID]] = $pageID;
348
349
                $parentTitle = str_replace([$homepageTitle . ' → ', $homepageTitle], '', $titles[$parentID] ?? '');
350
                $titles[$pageID] = trim($parentTitle . ' → ' . $page['title'], ' → ');
351
            }
352
        }
353
354
        foreach ($sequences as $pageGroupSortList) {
355
            ksort($pageGroupSortList);
356
357
            foreach ($pageGroupSortList as $id) {
358
                if (isset($titles[$id])) {
359
                    $pages[$id] = $titles[$id];
360
                }
361
            }
362
        }
363
364
        return $pages;
365
    }
366
367
    private static function getSubTreeForDropdown(
368
        array $navigation,
369
        int $parentId,
370
        string $parentTitle,
371
        callable $attributesFunction
372
    ): array {
373
        if (!isset($navigation['page'][$parentId]) || empty($navigation['page'][$parentId])) {
374
            return [];
375
        }
376
        $tree = self::getEmptyTreeArray();
377
378
        foreach ($navigation['page'][$parentId] as $page) {
379
            $pageId = $page['page_id'];
380
            $pageTitle = $parentTitle . ' → ' . $page['navigation_title'];
381
            $tree['pages'][$pageId] = $pageTitle;
382
            $tree['attributes'][$pageId] = $attributesFunction($page);
383
384
            $tree = self::mergeTreeForDropdownArrays(
385
                $tree,
386
                self::getSubTreeForDropdown($navigation, $pageId, $pageTitle, $attributesFunction)
387
            );
388
        }
389
390
        return $tree;
391
    }
392
393
    private static function mergeTreeForDropdownArrays(array $tree, array $subTree, string $treeLabel = null): array
394
    {
395
        if (empty($subTree)) {
396
            return $tree;
397
        }
398
399
        $tree['attributes'] += $subTree['attributes'];
400
401
        if ($treeLabel === null) {
402
            $tree['pages'] += $subTree['pages'];
403
404
            return $tree;
405
        }
406
407
        $tree['pages'][$treeLabel] += $subTree['pages'];
408
409
        return $tree;
410
    }
411
412
    private static function getEmptyTreeArray(): array
413
    {
414
        return [
415
            'pages' => [],
416
            'attributes' => [],
417
        ];
418
    }
419
420
    private static function getAttributesFunctionForTreeName(
421
        string $treeName,
422
        string $treeLabel,
423
        int $currentPageId
424
    ): callable {
425
        return function (array $page) use ($treeName, $treeLabel, $currentPageId) {
426
            $isCurrentPage = $currentPageId === $page['page_id'];
427
428
            return [
429
                'data-tree-name' => $treeName,
430
                'data-tree-label' => $treeLabel,
431
                'data-allow-after' => (int) (!$isCurrentPage && $page['page_id'] !== Page::HOME_PAGE_ID),
432
                'data-allow-inside' => (int) (!$isCurrentPage && $page['allow_children']),
433
                'data-allow-before' => (int) (!$isCurrentPage && $page['page_id'] !== Page::HOME_PAGE_ID),
434
            ];
435
        };
436
    }
437
438
    private static function addMainPageToTreeForDropdown(
439
        array $tree,
440
        string $branchLabel,
441
        callable $attributesFunction,
442
        array $page,
443
        array $navigation
444
    ): array {
445
        $tree['pages'][$branchLabel][$page['page_id']] = $page['navigation_title'];
446
        $tree['attributes'][$page['page_id']] = $attributesFunction($page);
447
448
        return self::mergeTreeForDropdownArrays(
449
            $tree,
450
            self::getSubTreeForDropdown(
451
                $navigation,
452
                $page['page_id'],
453
                $page['navigation_title'],
454
                $attributesFunction
455
            ),
456
            BL::lbl('MainNavigation')
457
        );
458
    }
459
460
    public static function getMoveTreeForDropdown(int $currentPageId, Locale $locale = null): array
461
    {
462
        $navigation = static::getCacheBuilder()->getNavigation($locale ?? Locale::workingLocale());
463
464
        $tree = self::addMainPageToTreeForDropdown(
465
            self::getEmptyTreeArray(),
466
            BL::lbl('MainNavigation'),
467
            self::getAttributesFunctionForTreeName('main', BL::lbl('MainNavigation'), $currentPageId),
468
            $navigation['page'][0][Page::HOME_PAGE_ID],
469
            $navigation
470
        );
471
472
        $treeBranches = [];
473
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
474
            $treeBranches['meta'] = BL::lbl('Meta');
475
        }
476
        $treeBranches['footer'] = BL::lbl('Footer');
477
        $treeBranches['root'] = BL::lbl('Root');
478
479
        foreach ($treeBranches as $branchName => $branchLabel) {
480
            if (!isset($navigation[$branchName][0]) || !is_array($navigation[$branchName][0])) {
481
                continue;
482
            }
483
484
            foreach ($navigation[$branchName][0] as $page) {
485
                $tree = self::addMainPageToTreeForDropdown(
486
                    $tree,
487
                    $branchLabel,
488
                    self::getAttributesFunctionForTreeName($branchName, $branchLabel, $currentPageId),
489
                    $page,
490
                    $navigation
491
                );
492
            }
493
        }
494
495
        return $tree;
496
    }
497
498
    private static function getSubtree(Type $type, array $navigation, int $parentId): ?array
499
    {
500
        $subPages = $navigation[(string) $type][$parentId] ?? null;
501
502
        if ($subPages === null || count($subPages) === 0) {
503
            return null;
504
        }
505
506
        $subTree = [];
507
        foreach ($subPages as $page) {
508
            $subTree[$page['page_id']] = [
509
                'attr' => [
510
                    'rel' => $page['tree_type'],
511
                    'data-jstree' => '{"type":"' . $page['tree_type'] . '"}',
512
                ],
513
                'page' => $page,
514
                'children' => self::getSubtree(Type::page(), $navigation, $page['page_id']),
515
            ];
516
        }
517
518
        return $subTree;
519
    }
520
521
    /**
522
     * Get all pages/level
523
     *
524
     * @param int[] $ids The parentIds.
525
     * @param array $data A holder for the generated data.
526
     * @param int $level The counter for the level.
527
     * @param AbstractLocale $locale
528
     *
529
     * @return array
530
     */
531
    public static function getTree(array $ids, array $data = null, int $level = 1, AbstractLocale $locale = null): array
532
    {
533
        $locale = $locale ?? Locale::workingLocale();
534
535
        /** @var PageRepository $pageRepository */
536
        $pageRepository = BackendModel::get(PageRepository::class);
537
        $data[$level] = $pageRepository->getPageTree($ids, $locale);
538
539
        // get the childIDs
540
        $childIds = array_keys($data[$level]);
541
542
        // build array
543
        if (!empty($data[$level])) {
544
            $data[$level] = array_map(
545
                function ($page) {
546
                    $page['has_extra'] = (bool) $page['has_extra'];
547
                    $page['has_children'] = (bool) $page['has_children'];
548
                    $page['allow_move'] = (bool) $page['allow_move'];
549
550
                    return $page;
551
                },
552
                $data[$level]
553
            );
554
555
            return self::getTree($childIds, $data, ++$level, $locale);
556
        }
557
558
        unset($data[$level]);
559
560
        return $data;
561
    }
562
563
    public static function getTreeHTML(): string
564
    {
565
        $navigation = static::getCacheBuilder()->getNavigation(Locale::workingLocale());
566
567
        $tree = [];
568
569
        $tree['main'] = [
570
            'name' => 'main',
571
            'label' => 'MainNavigation',
572
            'pages' => self::getSubtree(Type::page(), $navigation, 0),
573
        ];
574
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
575
            $tree['meta'] = [
576
                'name' => 'meta',
577
                'label' => 'Meta',
578
                'pages' => self::getSubtree(Type::meta(), $navigation, 0),
579
            ];
580
        }
581
        $tree['footer'] = [
582
            'name' => 'footer',
583
            'label' => 'Footer',
584
            'pages' => self::getSubtree(Type::footer(), $navigation, 0),
585
        ];
586
        $tree['root'] = [
587
            'name' => 'root',
588
            'label' => 'Root',
589
            'pages' => self::getSubtree(Type::root(), $navigation, 0),
590
        ];
591
592
        // return
593
        return BackendModel::getContainer()->get('templating')->render(
594
            BACKEND_MODULES_PATH . '/Pages/Resources/views/NavigationTree.html.twig',
595
            [
596
                'editUrl' => BackendModel::createUrlForAction('PageEdit', 'Pages'),
597
                'tree' => $tree,
598
            ]
599
        );
600
    }
601
602
    private static function pageIsChildOfParent(array $navigation, int $childId, int $parentId): bool
603
    {
604
        if (isset($navigation['page'][$parentId]) && !empty($navigation['page'][$parentId])) {
605
            foreach ($navigation['page'][$parentId] as $page) {
606
                if ($page['page_id'] === $childId) {
607
                    return true;
608
                }
609
610
                if (self::pageIsChildOfParent($navigation, $childId, $page['page_id'])) {
611
                    return true;
612
                }
613
            }
614
        }
615
616
        return false;
617
    }
618
619
    public static function getTreeNameForPageId(int $pageId): ?string
620
    {
621
        $navigation = static::getCacheBuilder()->getNavigation(Locale::workingLocale());
622
623
        if ($pageId === Page::HOME_PAGE_ID || self::pageIsChildOfParent($navigation, $pageId, Page::HOME_PAGE_ID)) {
624
            return 'main';
625
        }
626
627
        $treeNames = ['footer', 'root'];
628
629
        // only show meta if needed
630
        if (BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
631
            $treeNames[] = 'meta';
632
        }
633
634
        foreach ($treeNames as $treeName) {
635
            if (isset($navigation[$treeName][0]) && !empty($navigation[$treeName][0])) {
636
                // loop the items
637
                foreach ($navigation[$treeName][0] as $page) {
638
                    if ($pageId === $page['page_id']
639
                        || self::pageIsChildOfParent($navigation, $pageId, $page['page_id'])) {
640
                        return $treeName;
641
                    }
642
                }
643
            }
644
        }
645
646
        return null;
647
    }
648
649
    public static function getTypes(): array
650
    {
651
        return [
652
            'rich_text' => BL::lbl('Editor'),
653
            'block' => BL::lbl('Module'),
654
            'widget' => BL::lbl('Widget'),
655
            'usertemplate' => BL::lbl('UserTemplate'),
656
        ];
657
    }
658
659
    /**
660
     * @deprecated use the repository
661
     */
662
    public static function getUrl(string $url, int $id = null, int $parentId = null, bool $isAction = false): string
663
    {
664
        /** @var PageRepository $pageRepository */
665
        $pageRepository = BackendModel::get(PageRepository::class);
666
667
        return $pageRepository->getUrl($url, Locale::workingLocale(), $id, $parentId, $isAction);
668
    }
669
670
    /**
671
     * Insert multiple blocks at once
672
     *
673
     * @param array $blocks The blocks to insert.
674
     */
675
    public static function insertBlocks(array $blocks): void
676
    {
677
        if (empty($blocks)) {
678
            return;
679
        }
680
681
        /** @var PageBlockRepository $pageBlockRepository */
682
        $pageBlockRepository = BackendModel::get(PageBlockRepository::class);
683
684
        /** @var PageRepository $pageRepository */
685
        $pageRepository = BackendModel::get(PageRepository::class);
686
687
        // loop blocks
688
        foreach ($blocks as $block) {
689
            $extraId = $block['extra_id'];
690
691
            if (!isset($block['page']) && isset($block['revision_id'])) {
692
                $block['page'] = $pageRepository->find($block['revision_id']);
693
            }
694
695
            $pageBlock = new PageBlock(
696
                $block['page'],
697
                $block['position'],
698
                $extraId,
699
                new PageBlockType($block['extra_type']),
700
                $block['extra_data'],
701
                $block['html'],
702
                $block['visible'],
703
                $block['sequence']
704
            );
705
706
            $pageBlockRepository->add($pageBlock);
707
            $pageBlockRepository->save($pageBlock);
708
        }
709
    }
710
711
    public static function loadUserTemplates(): array
712
    {
713
        $themePath = FRONTEND_PATH . '/Themes/';
714
        $themePath .= BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork');
715
        $filePath = $themePath . '/Core/Layout/Templates/UserTemplates/Templates.json';
716
717
        $userTemplates = [];
718
719
        $fs = new Filesystem();
720
        if ($fs->exists($filePath)) {
721
            $userTemplates = json_decode(file_get_contents($filePath), true);
722
723
            foreach ($userTemplates as &$userTemplate) {
724
                $userTemplate['file'] =
725
                    '/src/Frontend/Themes/' .
726
                    BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork') .
727
                    '/Core/Layout/Templates/UserTemplates/' .
728
                    $userTemplate['file'];
729
            }
730
        }
731
732
        return $userTemplates;
733
    }
734
735
    /**
736
     * Move a page
737
     *
738
     * @param int $pageId The id for the page that has to be moved.
739
     * @param int $droppedOnPageId The id for the page where to page has been dropped on.
740
     * @param string $typeOfDrop The type of drop, possible values are: before, after, inside.
741
     * @param string $tree The tree the item is dropped on, possible values are: main, meta, footer, root.
742
     * @param Locale $locale The locale to use, if not provided we will use the working locale.
743
     *
744
     * @return bool
745
     */
746
    public static function move(
747
        int $pageId,
748
        int $droppedOnPageId,
749
        string $typeOfDrop,
750
        string $tree,
751
        Locale $locale = null
752
    ): bool {
753
        $typeOfDrop = \SpoonFilter::getValue($typeOfDrop, self::POSSIBLE_TYPES_OF_DROP, self::TYPE_OF_DROP_INSIDE);
754
        $tree = \SpoonFilter::getValue($tree, ['main', 'meta', 'footer', 'root'], 'root');
755
        $locale = $locale ?? Locale::workingLocale();
756
757
        // When dropping on the main navigation it should be added as a child of the home page
758
        if ($tree === 'main' && $droppedOnPageId === 0) {
759
            $droppedOnPageId = Page::HOME_PAGE_ID;
760
            $typeOfDrop = self::TYPE_OF_DROP_INSIDE;
761
        }
762
763
        // reset type of drop for special pages
764
        if ($droppedOnPageId === Page::HOME_PAGE_ID || $droppedOnPageId === Page::NO_PARENT_PAGE_ID) {
765
            $typeOfDrop = self::TYPE_OF_DROP_INSIDE;
766
        }
767
768
        $page = self::get($pageId, null, $locale);
769
        $droppedOnPage = self::get(
770
            ($droppedOnPageId === Page::NO_PARENT_PAGE_ID ? Page::HOME_PAGE_ID : $droppedOnPageId),
771
            null,
772
            $locale
773
        );
774
775
        if (empty($page) || empty($droppedOnPage)) {
776
            return false;
777
        }
778
779
        try {
780
            $newParent = self::getNewParent($droppedOnPageId, $typeOfDrop, $droppedOnPage);
781
        } catch (InvalidArgumentException $invalidArgumentException) {
782
            // parent doesn't allow children
783
            return false;
784
        }
785
786
        self::recalculateSequenceAfterMove(
787
            $typeOfDrop,
788
            self::getNewType($droppedOnPageId, $tree, $newParent, $droppedOnPage),
789
            $pageId,
790
            $locale,
791
            $newParent,
792
            $droppedOnPage['id']
793
        );
794
795
        self::updateUrlAfterMove($pageId, $page, $newParent);
0 ignored issues
show
Bug introduced by
$newParent of type string is incompatible with the type integer expected by parameter $newParent of Backend\Modules\Pages\En...l::updateUrlAfterMove(). ( Ignorable by Annotation )

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

795
        self::updateUrlAfterMove($pageId, $page, /** @scrutinizer ignore-type */ $newParent);
Loading history...
796
797
        return true;
798
    }
799
800
    public static function update(array $page): int
801
    {
802
        /** @var PageRepository $pageRepository */
803
        $pageRepository = BackendModel::get(PageRepository::class);
804
        /** @var MetaRepository $metaRepository */
805
        $metaRepository = BackendModel::get('fork.repository.meta');
806
807
        $locale = Locale::fromString($page['locale']);
808
809
        if (self::isForbiddenToDelete($page['id'])) {
810
            $page['allow_delete'] = false;
811
        }
812
813
        if (self::isForbiddenToMove($page['id'])) {
814
            $page['allow_move'] = false;
815
        }
816
817
        if (self::isForbiddenToHaveChildren($page['id'])) {
818
            $page['allow_children'] = false;
819
        }
820
821
        // update old revisions
822
        if ($page['status'] !== (string) Status::draft()) {
823
            $pageEntities = $pageRepository->findBy(
824
                [
825
                    'id' => $page['id'],
826
                    'locale' => $locale,
827
                ]
828
            );
829
830
            foreach ($pageEntities as $pageEntity) {
831
                $pageEntity->archive();
832
                $pageRepository->save($pageEntity);
833
            }
834
        } else {
835
            $pageRepository->deleteByIdAndUserIdAndStatusAndLocale(
836
                (int) $page['id'],
837
                BackendAuthentication::getUser()->getUserId(),
838
                Status::draft(),
839
                $locale
840
            );
841
        }
842
843
        $meta = $metaRepository->find($page['meta_id']);
844
845
        $pageEntity = new Page(
846
            $page['id'],
847
            $page['user_id'],
848
            $page['parent_id'],
849
            $page['template_id'],
850
            clone $meta,
851
            $locale,
852
            $page['title'],
853
            $page['navigation_title'],
854
            null,
855
            $page['publish_on'],
856
            null,
857
            $page['sequence'],
858
            $page['navigation_title_overwrite'],
859
            $page['hidden'],
860
            new Status($page['status']),
861
            new Type($page['type']),
862
            $page['data'],
863
            $page['allow_move'],
864
            $page['allow_children'],
865
            $page['allow_edit'],
866
            $page['allow_delete']
867
        );
868
869
        $pageRepository->add($pageEntity);
870
        $pageRepository->save($pageEntity);
871
872
        // how many revisions should we keep
873
        $rowsToKeep = (int) BackendModel::get('fork.settings')->get('Pages', 'max_num_revisions', 20);
874
875
        // get revision-ids for items to keep
876
        $revisionIdsToKeep = $pageRepository->getRevisionIdsToKeep($page['id'], $rowsToKeep);
877
878
        // delete other revisions
879
        if (count($revisionIdsToKeep) !== 0) {
880
            // because blocks are linked by revision we should get all revisions we want to delete
881
882
            $revisionsToDelete = $pageRepository
883
                ->getRevisionIdsToDelete(
884
                    $page['id'],
885
                    Status::archive(),
886
                    $revisionIdsToKeep
887
                );
888
889
            // any revisions to delete
890
            if (count($revisionsToDelete) !== 0) {
891
                $pageRepository->deleteByRevisionIds($revisionsToDelete);
892
893
                /** @var PageBlockRepository $pageBlockRepository */
894
                $pageBlockRepository = BackendModel::get(PageBlockRepository::class);
895
                $pageBlockRepository->deleteByRevisionIds($revisionsToDelete);
896
            }
897
        }
898
899
        // return the new revision id
900
        return $pageEntity->getRevisionId();
901
    }
902
903
    /**
904
     * Switch templates for all existing pages
905
     *
906
     * @param int $oldTemplateId The id of the new template to replace.
907
     * @param int $newTemplateId The id of the new template to use.
908
     * @param bool $overwrite Overwrite all pages with default blocks.
909
     */
910
    public static function updatePagesTemplates(int $oldTemplateId, int $newTemplateId, bool $overwrite = false): void
911
    {
912
        // fetch new template data
913
        $newTemplate = BackendExtensionsModel::getTemplate($newTemplateId);
914
        $newTemplate['data'] = @unserialize($newTemplate['data'], ['allowed_classes' => 'false']);
915
916
        // fetch all pages
917
        /** @var PageRepository $pageRepository */
918
        $pageRepository = BackendModel::get(PageRepository::class);
919
        $pages = $pageRepository->findBy(
920
            ['templateId' => $oldTemplateId, 'status' => [Status::active(), Status::draft()]]
921
        );
922
923
        // there is no active/draft page with the old template id
924
        if (count($pages) === 0) {
925
            return;
926
        }
927
928
        // loop pages
929
        /** @var Page $page */
930
        foreach ($pages as $page) {
931
            // fetch blocks
932
            $blocksContent = self::getBlocks($page->getId(), $page->getRevisionId(), $page->getLocale());
933
934
            // save new page revision
935
            $newPageRevisionId = self::update(
936
                [
937
                    'id' => $page->getId(),
938
                    'user_id' => $page->getUserId(),
939
                    'parent_id' => $page->getParentId(),
940
                    'template_id' => $newTemplateId,
941
                    'meta_id' => $page->getMeta()->getId(),
942
                    'locale' => $page->getLocale(),
943
                    'title' => $page->getTitle(),
944
                    'navigation_title' => $page->getNavigationTitle(),
945
                    'publish_on' => $page->getPublishOn(),
946
                    'publish_until' => $page->getPublishUntil(),
947
                    'sequence' => $page->getSequence(),
948
                    'navigation_title_overwrite' => $page->isNavigationTitleOverwrite(),
949
                    'hidden' => $page->isHidden(),
950
                    'status' => (string) $page->getStatus(),
951
                    'type' => (string) $page->getType(),
952
                    'data' => $page->getData(),
953
                    'allow_move' => $page->isAllowMove(),
954
                    'allow_children' => $page->isAllowChildren(),
955
                    'allow_edit' => $page->isAllowEdit(),
956
                    'allow_delete' => $page->isAllowDelete(),
957
                ]
958
            );
959
960
            // overwrite all blocks with current defaults
961
            if ($overwrite) {
962
                $blocksContent = [];
963
964
                // fetch default blocks for this page
965
                $defaultBlocks = [];
966
                if (isset($newTemplate['data']['default_extras_' . $page->getLocale()])) {
967
                    $defaultBlocks = $newTemplate['data']['default_extras_' . $page->getLocale()];
968
                } elseif (isset($newTemplate['data']['default_extras'])) {
969
                    $defaultBlocks = $newTemplate['data']['default_extras'];
970
                }
971
972
                // loop positions
973
                foreach ($defaultBlocks as $position => $blocks) {
974
                    // loop blocks
975
                    foreach ($blocks as $extraId) {
976
                        // add to the list
977
                        $blocksContent[] = [
978
                            'revision_id' => $newPageRevisionId,
979
                            'position' => $position,
980
                            'extra_id' => $extraId,
981
                            'extra_type' => 'rich_text',
982
                            'html' => '',
983
                            'created_on' => BackendModel::getUTCDate(),
984
                            'edited_on' => BackendModel::getUTCDate(),
985
                            'visible' => true,
986
                            'sequence' => count($defaultBlocks[$position]) - 1,
987
                        ];
988
                    }
989
                }
990
            } else {
991
                // don't overwrite blocks, just re-use existing
992
                // set new page revision id
993
                foreach ($blocksContent as &$block) {
994
                    $block['revision_id'] = $newPageRevisionId;
995
                    $block['created_on'] = BackendModel::getUTCDate(null, $block['created_on']->getTimestamp());
996
                    $block['edited_on'] = BackendModel::getUTCDate(null, $block['edited_on']->getTimestamp());
997
                }
998
            }
999
1000
            // insert the blocks
1001
            self::insertBlocks($blocksContent);
1002
        }
1003
    }
1004
1005
    public static function getEncodedRedirectUrl(string $redirectUrl): string
1006
    {
1007
        preg_match('!(http[s]?)://(.*)!i', $redirectUrl, $matches);
1008
        $urlChunks = explode('/', $matches[2]);
1009
        if (!empty($urlChunks)) {
1010
            // skip domain name
1011
            $domain = array_shift($urlChunks);
1012
            foreach ($urlChunks as &$urlChunk) {
1013
                $urlChunk = rawurlencode($urlChunk);
1014
            }
1015
            unset($urlChunk);
1016
            $redirectUrl = $matches[1] . '://' . $domain . '/' . implode('/', $urlChunks);
1017
        }
1018
1019
        return $redirectUrl;
1020
    }
1021
1022
    private static function getNewParent(int $droppedOnPageId, string $typeOfDrop, array $droppedOnPage): int
1023
    {
1024
        if ($droppedOnPageId === Page::NO_PARENT_PAGE_ID) {
1025
            return Page::NO_PARENT_PAGE_ID;
1026
        }
1027
1028
        if ($typeOfDrop === self::TYPE_OF_DROP_INSIDE) {
1029
            // check if item allows children
1030
            if (!$droppedOnPage['allow_children']) {
1031
                throw new InvalidArgumentException('Parent page is not allowed to have child pages');
1032
            }
1033
1034
            return $droppedOnPage['id'];
1035
        }
1036
1037
        // if the item has to be moved before or after
1038
        return $droppedOnPage['parent_id'];
1039
    }
1040
1041
    private static function getNewType(int $droppedOnPageId, string $tree, int $newParent, array $droppedOnPage): Type
1042
    {
1043
        if ($droppedOnPageId === Page::NO_PARENT_PAGE_ID) {
1044
            if ($tree === 'footer') {
1045
                return Type::footer();
1046
            }
1047
1048
            if ($tree === 'meta') {
1049
                return Type::meta();
1050
            }
1051
1052
            return Type::root();
1053
        }
1054
1055
        if ($newParent === Page::NO_PARENT_PAGE_ID) {
1056
            return $droppedOnPage['type'];
1057
        }
1058
1059
        return Type::page();
1060
    }
1061
1062
    private static function recalculateSequenceAfterMove(
1063
        string $typeOfDrop,
1064
        Type $newType,
1065
        int $pageId,
1066
        Locale $locale,
1067
        string $newParent,
1068
        int $droppedOnPageId
1069
    ): void {
1070
        /** @var PageRepository $pageRepository */
1071
        $pageRepository = BackendModel::get(PageRepository::class);
1072
1073
        // calculate new sequence for items that should be moved inside
1074
        if ($typeOfDrop === self::TYPE_OF_DROP_INSIDE) {
1075
            $newSequence = $pageRepository->getNewSequenceForMove((int) $newParent, $locale);
1076
1077
            $pages = $pageRepository->findBy(['id' => $pageId, 'locale' => $locale, 'status' => Status::active()]);
1078
1079
            foreach ($pages as $page) {
1080
                $page->move((int) $newParent, $newSequence, $newType);
1081
                $pageRepository->save($page);
1082
            }
1083
1084
            return;
1085
        }
1086
1087
        $droppedOnPage = $pageRepository
1088
            ->findOneBy(
1089
                [
1090
                    'id' => $droppedOnPageId,
1091
                    'locale' => $locale,
1092
                    'status' => Status::active(),
1093
                ]
1094
            );
1095
1096
        if (!$droppedOnPage instanceof Page) {
1097
            throw new RuntimeException('Drop on page not found');
1098
        }
1099
1100
        // calculate new sequence for items that should be moved before or after
1101
        $droppedOnPageSequence = $droppedOnPage->getSequence();
1102
1103
        $newSequence = $droppedOnPageSequence + ($typeOfDrop === self::TYPE_OF_DROP_BEFORE ? -1 : 1);
1104
1105
        // increment all pages with a sequence that is higher than the new sequence;
1106
        $pageRepository->incrementSequence($newParent, $locale, $newSequence);
0 ignored issues
show
Bug introduced by
$newParent of type string is incompatible with the type integer expected by parameter $parentId of Backend\Modules\Pages\Do...ry::incrementSequence(). ( Ignorable by Annotation )

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

1106
        $pageRepository->incrementSequence(/** @scrutinizer ignore-type */ $newParent, $locale, $newSequence);
Loading history...
1107
1108
        $pages = $pageRepository->findBy(['id' => $pageId, 'locale' => $locale, 'status' => Status::active()]);
1109
1110
        foreach ($pages as $page) {
1111
            $page->move((int) $newParent, $newSequence, $newType);
1112
            $pageRepository->save($page);
1113
        }
1114
    }
1115
1116
    private static function updateUrlAfterMove(int $pageId, array $page, int $newParent): void
1117
    {
1118
        /** @var MetaRepository $metaRepository */
1119
        $metaRepository = BackendModel::get('fork.repository.meta');
1120
        $meta = $metaRepository->find($page['meta_id']);
1121
1122
        if (!$meta instanceof Meta) {
1123
            return;
1124
        }
1125
1126
        $newUrl = self::getUrl(
0 ignored issues
show
Deprecated Code introduced by
The function Backend\Modules\Pages\Engine\Model::getUrl() has been deprecated: use the repository ( Ignorable by Annotation )

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

1126
        $newUrl = /** @scrutinizer ignore-deprecated */ self::getUrl(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1127
            $meta->getUrl(),
1128
            $pageId,
1129
            $newParent,
1130
            isset($page['data']['is_action']) && $page['data']['is_action']
1131
        );
1132
1133
        $meta->update(
1134
            $meta->getKeywords(),
1135
            $meta->isKeywordsOverwrite(),
1136
            $meta->getDescription(),
1137
            $meta->isDescriptionOverwrite(),
1138
            $meta->getTitle(),
1139
            $meta->isTitleOverwrite(),
1140
            $newUrl,
1141
            $meta->isUrlOverwrite()
1142
        );
1143
1144
        $metaRepository->save($meta);
1145
    }
1146
}
1147