Model::copy()   F
last analyzed

Complexity

Conditions 19
Paths 872

Size

Total Lines 231
Code Lines 121

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 121
c 2
b 0
f 0
nc 872
nop 2
dl 0
loc 231
rs 0.4222
ccs 0
cts 109
cp 0
crap 380

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\FormBuilder\Command\CopyFormWidgetsToOtherLocale;
7
use Backend\Modules\Location\Command\CopyLocationWidgetsToOtherLocale;
8
use Common\Doctrine\Entity\Meta;
9
use ForkCMS\Utility\Thumbnails;
10
use SimpleBus\Message\Bus\MessageBus;
11
use InvalidArgumentException;
12
use SpoonFilter;
13
use Symfony\Component\Filesystem\Filesystem;
14
use Backend\Core\Engine\Authentication as BackendAuthentication;
15
use Backend\Core\Language\Language as BL;
16
use Backend\Core\Engine\Model as BackendModel;
17
use Backend\Core\Language\Locale;
18
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
19
use Backend\Modules\Search\Engine\Model as BackendSearchModel;
20
use Backend\Modules\Tags\Engine\Model as BackendTagsModel;
21
use ForkCMS\App\ForkController;
22
use Frontend\Core\Language\Language as FrontendLanguage;
23
24
/**
25
 * In this file we store all generic functions that we will be using in the PagesModule
26
 */
27
class Model
28
{
29
    const NO_PARENT_PAGE_ID = 0;
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 pages AS i
43
         WHERE i.status = ? AND i.language = ?
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 pages AS i
50
         INNER JOIN
51
         (
52
             SELECT MAX(i.revision_id) AS revision_id
53
             FROM pages AS i
54
             WHERE i.status = ? AND i.user_id = ? AND i.language = ?
55
             GROUP BY i.id
56
         ) AS p
57
         WHERE i.revision_id = p.revision_id';
58
59
    const QUERY_BROWSE_REVISIONS =
60
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
61
         FROM pages AS i
62
         WHERE i.id = ? AND i.status = ? AND i.language = ?
63
         ORDER BY i.edited_on DESC';
64
65
    const QUERY_DATAGRID_BROWSE_SPECIFIC_DRAFTS =
66
        'SELECT i.id, i.revision_id, i.title, UNIX_TIMESTAMP(i.edited_on) AS edited_on, i.user_id
67
         FROM pages AS i
68
         WHERE i.id = ? AND i.status = ? AND i.language = ?
69
         ORDER BY i.edited_on DESC';
70
71
    const QUERY_BROWSE_TEMPLATES =
72
        'SELECT i.id, i.label AS title
73
         FROM pages_templates AS i
74 38
         WHERE i.theme = ?
75
         ORDER BY i.label ASC';
76 38
77 38
    public static function getCacheBuilder(): CacheBuilder
78 38
    {
79
        static $cacheBuilder = null;
80
        if ($cacheBuilder === null) {
81 38
            $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('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

81
            $cacheBuilder = new CacheBuilder(BackendModel::get('database'), /** @scrutinizer ignore-type */ BackendModel::get('cache.pool'));
Loading history...
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

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

1213
        $fullUrl = self::getFullUrl(/** @scrutinizer ignore-type */ $parentId) . '/' . $url;
Loading history...
1214
1215
        // get info about parent page
1216
        $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

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

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

1703
        $imageFilename = $metaUrl . '_' . time() . '.' . /** @scrutinizer ignore-type */ $extension;
Loading history...
1704
        $newImagePath = $imagePath . '/source/' . $imageFilename;
1705
1706
        // make sure we have a separate image for the copy in case the original image gets removed
1707
        (new Filesystem())->copy($originalImagePath, $newImagePath);
1708
        BackendModel::get(Thumbnails::class)->generate($imagePath, $newImagePath);
1709
1710
        return $imageFilename;
1711
    }
1712
}
1713