Issues (281)

Branch: master

src/Backend/Modules/Pages/Actions/Edit.php (1 issue)

1
<?php
2
3
namespace Backend\Modules\Pages\Actions;
4
5
use Backend\Core\Engine\Authentication;
6
use Backend\Core\Engine\Base\ActionEdit as BackendBaseActionEdit;
7
use Backend\Core\Engine\Authentication as BackendAuthentication;
8
use Backend\Core\Engine\DataGridDatabase as BackendDataGridDatabase;
9
use Backend\Core\Engine\DataGridFunctions as BackendDataGridFunctions;
10
use Backend\Core\Engine\Form as BackendForm;
11
use Backend\Core\Language\Language as BL;
12
use Backend\Core\Engine\Meta as BackendMeta;
13
use Backend\Core\Engine\Model as BackendModel;
14
use Backend\Form\Type\DeleteType;
15
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
16
use Backend\Modules\Pages\Engine\Model as BackendPagesModel;
17
use Backend\Modules\Search\Engine\Model as BackendSearchModel;
18
use Backend\Modules\Tags\Engine\Model as BackendTagsModel;
19
use Backend\Modules\Profiles\Engine\Model as BackendProfilesModel;
20
use ForkCMS\Utility\Thumbnails;
21
use SpoonFormHidden;
22
use Symfony\Component\HttpFoundation\Response;
23
24
/**
25
 * This is the edit-action, it will display a form to update an item
26
 */
27
class Edit extends BackendBaseActionEdit
28
{
29
    /**
30
     * The blocks linked to this page
31
     *
32
     * @var array
33
     */
34
    private $blocksContent = [];
35
36
    /**
37
     * DataGrid for the drafts
38
     *
39
     * @var BackendDataGridDatabase
40
     */
41
    private $dgDrafts;
42
43
    /**
44
     * The extras
45
     *
46
     * @var array
47
     */
48
    private $extras = [];
49
50
    /**
51
     * The hreflang fields
52
     *
53
     * @var array
54
     */
55
    private $hreflangFields = [];
56
57
    /**
58
     * Is the current user a god user?
59
     *
60
     * @var bool
61
     */
62
    private $isGod = false;
63
64
    /**
65
     * The positions
66
     *
67
     * @var array
68
     */
69
    private $positions = [];
70
71
    /**
72
     * The template data
73
     *
74
     * @var array
75
     */
76
    private $templates = [];
77
78
    /**
79
     * @var string
80
     */
81
    private $oldUrl;
82
83
    public function execute(): void
84
    {
85
        parent::execute();
86
87
        // load record
88
        $this->loadData();
89
90
        // add js
91
        $this->header->addJS('jstree/jquery.tree.js', null, false);
92
        $this->header->addJS('jstree/lib/jquery.cookie.js', null, false);
93
        $this->header->addJS('jstree/plugins/jquery.tree.cookie.js', null, false);
94
        $this->header->addJS('/js/vendors/SimpleAjaxUploader.min.js', 'Core', false, true);
95
96
        // get the templates
97
        $this->templates = BackendExtensionsModel::getTemplates();
98
99
        // set the default template as checked
100
        $this->templates[$this->record['template_id']]['checked'] = true;
101
102
        // homepage?
103
        if ($this->id == BackendModel::HOME_PAGE_ID) {
104
            // loop and set disabled state
105
            foreach ($this->templates as &$row) {
106
                $row['disabled'] = ($row['has_block']);
107
            }
108
        }
109
110
        // get the extras
111
        $this->extras = BackendExtensionsModel::getExtras();
112
113
        $this->loadForm();
114
        $this->loadDrafts();
115
        $this->loadRevisions();
116
        $this->validateForm();
117
        $this->loadDeleteForm();
118
        $this->parse();
119
        $this->display();
120
    }
121
122
    private function loadData(): void
123
    {
124
        // get record
125
        $this->id = $this->getRequest()->query->getInt('id');
126
        $this->isGod = BackendAuthentication::getUser()->isGod();
127
128
        // check if something went wrong
129
        if ($this->id === 0 || !BackendPagesModel::exists($this->id)) {
130
            $this->redirect(
131
                BackendModel::createUrlForAction('Index') . '&error=non-existing'
132
            );
133
        }
134
135
        // get the record
136
        $this->record = BackendPagesModel::get($this->id);
137
138
        // load blocks
139
        $this->blocksContent = BackendPagesModel::getBlocks($this->id, $this->record['revision_id']);
140
141
        // is there a revision specified?
142
        $revisionToLoad = $this->getRequest()->query->getInt('revision');
143
144
        // if this is a valid revision
145
        if ($revisionToLoad !== 0) {
146
            // overwrite the current record
147
            $this->record = (array) BackendPagesModel::get($this->id, $revisionToLoad);
148
149
            // load blocks
150
            $this->blocksContent = BackendPagesModel::getBlocks($this->id, $revisionToLoad);
151
152
            // show warning
153
            $this->template->assign('appendRevision', true);
154
        }
155
156
        // is there a revision specified?
157
        $draftToLoad = $this->getRequest()->query->getInt('draft');
158
159
        // if this is a valid revision
160
        if ($draftToLoad !== 0) {
161
            // overwrite the current record
162
            $this->record = (array) BackendPagesModel::get($this->id, $draftToLoad);
163
164
            // load blocks
165
            $this->blocksContent = BackendPagesModel::getBlocks($this->id, $draftToLoad);
166
167
            // show warning
168
            $this->template->assign('appendRevision', true);
169
        }
170
171
        // reset some vars
172
        $this->record['full_url'] = BackendPagesModel::getFullUrl($this->record['id']);
173
    }
174
175
    private function loadDrafts(): void
176
    {
177
        // create datagrid
178
        $this->dgDrafts = new BackendDataGridDatabase(
179
            BackendPagesModel::QUERY_DATAGRID_BROWSE_SPECIFIC_DRAFTS,
180
            [$this->record['id'], 'draft', BL::getWorkingLanguage()]
181
        );
182
183
        // hide columns
184
        $this->dgDrafts->setColumnsHidden(['id', 'revision_id']);
185
186
        // disable paging
187
        $this->dgDrafts->setPaging(false);
188
189
        // set headers
190
        $this->dgDrafts->setHeaderLabels(
191
            [
192
                 'user_id' => \SpoonFilter::ucfirst(BL::lbl('By')),
193
                 'edited_on' => \SpoonFilter::ucfirst(BL::lbl('LastEditedOn')),
194
            ]
195
        );
196
197
        // set column-functions
198
        $this->dgDrafts->setColumnFunction([new BackendDataGridFunctions(), 'getUser'], ['[user_id]'], 'user_id');
199
        $this->dgDrafts->setColumnFunction(
200
            [new BackendDataGridFunctions(), 'getTimeAgo'],
201
            ['[edited_on]'],
202
            'edited_on'
203
        );
204
        $this->dgDrafts->setColumnFunction('htmlspecialchars', ['[title]'], 'title', false);
205
206
        // our JS needs to know an id, so we can highlight it
207
        $this->dgDrafts->setRowAttributes(['id' => 'row-[revision_id]']);
208
209
        // check if this action is allowed
210
        if (BackendAuthentication::isAllowedAction('Edit')) {
211
            // set column URLs
212
            $this->dgDrafts->setColumnURL(
213
                'title',
214
                BackendModel::createUrlForAction('Edit') . '&amp;id=[id]&amp;draft=[revision_id]'
215
            );
216
217
            // add use column
218
            $this->dgDrafts->addColumn(
219
                'use_draft',
220
                null,
221
                BL::lbl('UseThisDraft'),
222
                BackendModel::createUrlForAction('Edit') . '&amp;id=[id]&amp;draft=[revision_id]',
223
                BL::lbl('UseThisDraft')
224
            );
225
        }
226
    }
227
228
    private function loadForm(): void
229
    {
230
        // get default template id
231
        $defaultTemplateId = $this->get('fork.settings')->get('Pages', 'default_template', 1);
232
233
        // create form
234
        $this->form = new BackendForm('edit');
235
236
        // assign in template
237
        $this->template->assign('defaultTemplateId', $defaultTemplateId);
238
239
        // create elements
240
        $this->form->addText('title', $this->record['title'], null, 'form-control title', 'form-control danger title');
241
        $this->form->addEditor('html');
242
        $this->form->addHidden('template_id', $this->record['template_id']);
243
        $this->form->addRadiobutton(
244
            'hidden',
245
            [
246
                 ['label' => BL::lbl('Hidden'), 'value' => 1],
247
                 ['label' => BL::lbl('Published'), 'value' => 0],
248
            ],
249
            $this->record['hidden']
250
        );
251
252
        // image related fields
253
        $this->form->addImage('image')->setAttribute('data-fork-cms-role', 'image-field');
254
        $this->form->addCheckbox('remove_image');
255
256
        // move page fields
257
        $chkMovePage = $this->form->addCheckbox('move_page');
258
        $chkMovePage->setAttribute('data-role', 'move-page-toggle');
259
        if (!(bool) $this->record['allow_move']) {
260
            $chkMovePage->setAttribute('disabled');
261
            $chkMovePage->setAttribute('class', 'fork-form-checkbox disabled');
262
        }
263
264
        $movePageTreeOptions = [
265
            'main' => BL::lbl('MainNavigation'),
266
            'meta' => BL::lbl('Meta'),
267
            'footer' => BL::lbl('Footer'),
268
            'root' => BL::lbl('Root'),
269
        ];
270
        if (!BackendModel::get('fork.settings')->get('Pages', 'meta_navigation', false)) {
271
            unset($movePageTreeOptions['meta']);
272
        }
273
        $this->form->addDropdown(
274
            'move_page_tree',
275
            $movePageTreeOptions,
276
            BackendPagesModel::getTreeNameForPageId($this->id)
277
        )->setAttribute('data-role', 'move-page-tree-changer');
278
279
        $this->form->addDropdown(
280
            'move_page_type',
281
            [
282
                BackendPagesModel::TYPE_OF_DROP_BEFORE => BL::lbl('BeforePage'),
283
                BackendPagesModel::TYPE_OF_DROP_AFTER  => BL::lbl('AfterPage'),
284
                BackendPagesModel::TYPE_OF_DROP_INSIDE  => BL::lbl('InsidePage')
285
            ],
286
            BackendPagesModel::TYPE_OF_DROP_INSIDE
287
        )->setAttribute('data-role', 'move-page-type-changer');
288
        $dropdownPageTree = BackendPagesModel::getMoveTreeForDropdown($this->id);
289
290
        $ddmMovePageReferencePage = $this->form->addDropdown(
291
            'move_page_reference_page',
292
            (array) $dropdownPageTree['pages'],
293
            $this->record['parent_id'] != 0 ? $this->record['parent_id'] : null
294
        )->setDefaultElement(BL::lbl('AppendToTree'), 0)->setAttribute('data-role', 'move-page-pages-select');
295
        foreach ((array) $dropdownPageTree['attributes'] as $value => $attributes) {
296
            $ddmMovePageReferencePage->setOptionAttributes($value, $attributes);
297
        }
298
299
        // just execute if the site is multi-language
300
        if ($this->getContainer()->getParameter('site.multilanguage')) {
301
            // loop active languages
302
            foreach (BL::getActiveLanguages() as $language) {
303
                if ($language != BL::getWorkingLanguage()) {
304
                    $pages = BackendPagesModel::getPagesForDropdown($language);
305
                    // add field for each language
306
                    $field = $this->form->addDropdown('hreflang_' . $language, $pages, (!empty($this->record['data']['hreflang_' . $language]) ? $this->record['data']['hreflang_' . $language] : null))->setDefaultElement('');
307
                    $this->hreflangFields[$language]['field_hreflang'] = $field->parse();
308
                }
309
            }
310
        }
311
312
        // page auth related fields
313
        // check if profiles module is installed
314
        if (BackendModel::isModuleInstalled('Profiles')) {
315
            // add checkbox for auth_required
316
            $this->form->addCheckbox(
317
                'auth_required',
318
                isset($this->record['data']['auth_required']) && $this->record['data']['auth_required']
319
            );
320
321
            // add checkbox for index page to search
322
            $this->form->addCheckbox(
323
                'remove_from_search_index',
324
                isset($this->record['data']['remove_from_search_index']) && $this->record['data']['remove_from_search_index']
325
            );
326
327
            // get all groups and parse them in key value pair
328
            $groupItems = BackendProfilesModel::getGroups();
329
            if (!empty($groupItems)) {
330
                $groups = [];
331
                foreach ($groupItems as $key => $item) {
332
                    $groups[] = ['label' => $item, 'value' => $key];
333
                }
334
                // set checked values
335
                $checkedGroups = [];
336
                if (isset($this->record['data']['auth_groups']) && is_array($this->record['data']['auth_groups'])) {
337
                    foreach ($this->record['data']['auth_groups'] as $group) {
338
                        $checkedGroups[] = $group;
339
                    }
340
                }
341
                // add multi checkbox
342
                $this->form->addMultiCheckbox('auth_groups', $groups, $checkedGroups);
343
            }
344
        }
345
346
        // a god user should be able to adjust the detailed settings for a page easily
347
        if ($this->isGod) {
348
            $permissions = [
349
                'move' => ['data-role' => 'allow-move-toggle'],
350
                'children' => ['data-role' => 'allow-children-toggle'],
351
                'edit' => ['data-role' => 'allow-edit-toggle'],
352
                'delete' => ['data-role' => 'allow-delete-toggle'],
353
            ];
354
            if (BackendPagesModel::isForbiddenToMove($this->id)) {
355
                $permissions['move']['disabled'] = null;
356
            }
357
            if (BackendPagesModel::isForbiddenToHaveChildren($this->id)) {
358
                $permissions['children']['disabled'] = null;
359
            }
360
            if (BackendPagesModel::isForbiddenToDelete($this->id)) {
361
                $permissions['delete']['disabled'] = null;
362
            }
363
            $checked = [];
364
            $values = [];
365
366
            foreach ($permissions as $permission => $attributes) {
367
                $values[] = [
368
                    'label' => BL::msg(\SpoonFilter::toCamelCase('allow_' . $permission)),
369
                    'value' => $permission,
370
                    'attributes' => $attributes,
371
                ];
372
                if (isset($this->record['allow_' . $permission]) && $this->record['allow_' . $permission]) {
373
                    $checked[] = $permission;
374
                }
375
            }
376
377
            $this->form->addMultiCheckbox('allow', $values, $checked);
378
379
            // css link class
380
            $page['link_class'] = $this->form->addText('link_class', isset($this->record['data']['link_class']) ? $this->record['data']['link_class'] : null);
381
        }
382
383
        // build prototype block
384
        $block = [];
385
        $block['index'] = 0;
386
        $block['formElements']['chkVisible'] = $this->form->addCheckbox('block_visible_' . $block['index'], true);
387
        $block['formElements']['hidExtraId'] = $this->form->addHidden('block_extra_id_' . $block['index'], 0);
388
        $block['formElements']['hidExtraType'] = $this->form->addHidden('block_extra_type_' . $block['index'], 'rich_text');
389
        $block['formElements']['hidExtraData'] = $this->form->addHidden('block_extra_data_' . $block['index']);
390
        $block['formElements']['hidPosition'] = $this->form->addHidden('block_position_' . $block['index'], 'fallback');
391
        $block['formElements']['txtHTML'] = $this->form->addTextarea(
392
            'block_html_' . $block['index'],
393
            ''
394
        ); // this is no editor; we'll add the editor in JS
395
396
        // add default block to "fallback" position, the only one which we can rest assured to exist
397
        $this->positions['fallback']['blocks'][] = $block;
398
399
        // content has been submitted: re-create submitted content rather than the database-fetched content
400
        if ($this->getRequest()->request->has('block_html_0')) {
401
            $this->blocksContent = [];
402
            $hasBlock = false;
403
            $i = 1;
404
405
            // loop submitted blocks
406
            $positions = [];
407
            while ($this->getRequest()->request->has('block_position_' . $i)) {
408
                $block = [];
409
410
                // save block position
411
                $block['position'] = $this->getRequest()->request->get('block_position_' . $i);
412
                $positions[$block['position']][] = $block;
413
414
                // set linked extra
415
                $block['extra_id'] = $this->getRequest()->request->get('block_extra_id_' . $i);
416
                $block['extra_type'] = $this->getRequest()->request->get('block_extra_type_' . $i);
417
                $block['extra_data'] = $this->getRequest()->request->get('block_extra_data_' . $i);
418
419
                // reset some stuff
420
                if ($block['extra_id'] <= 0) {
421
                    $block['extra_id'] = null;
422
                }
423
424
                // init html
425
                $block['html'] = null;
426
427
                $html = $this->getRequest()->request->get('block_html_' . $i);
428
429
                // extra-type is HTML
430
                if ($block['extra_id'] === null || $block['extra_type'] == 'usertemplate') {
431
                    if ($this->getRequest()->request->get('block_extra_type_' . $i) === 'usertemplate') {
432
                        $block['extra_id'] = $this->getRequest()->request->get('block_extra_id_' . $i);
433
                        $_POST['block_extra_data_' . $i] = htmlspecialchars($_POST['block_extra_data_' . $i]);
434
                    } else {
435
                        // reset vars
436
                        $block['extra_id'] = null;
437
                    }
438
                    $block['html'] = $html;
439
                } else {
440
                    // type of block
441
                    if (isset($this->extras[$block['extra_id']]['type']) && $this->extras[$block['extra_id']]['type'] == 'block') {
442
                        // set error
443
                        if ($hasBlock) {
444
                            $this->form->addError(BL::err('CantAdd2Blocks'));
445
                        }
446
447
                        // home can't have blocks
448
                        if ($this->record['id'] == BackendModel::HOME_PAGE_ID) {
449
                            $this->form->addError(BL::err('HomeCantHaveBlocks'));
450
                        }
451
452
                        // reset var
453
                        $hasBlock = true;
454
                    }
455
                }
456
457
                // set data
458
                $block['created_on'] = BackendModel::getUTCDate();
459
                $block['edited_on'] = $block['created_on'];
460
                $block['visible'] = $this->getRequest()->request->getBoolean('block_visible_' . $i);
461
                $block['sequence'] = count($positions[$block['position']]) - 1;
462
463
                // add to blocks
464
                $this->blocksContent[] = $block;
465
466
                // increment counter; go fetch next block
467
                ++$i;
468
            }
469
        }
470
471
        // build blocks array
472
        foreach ($this->blocksContent as $i => $block) {
473
            $block['index'] = $i + 1;
474
            $block['formElements']['chkVisible'] = $this->form->addCheckbox(
475
                'block_visible_' . $block['index'],
476
                $block['visible']
477
            );
478
            $block['formElements']['hidExtraId'] = $this->form->addHidden(
479
                'block_extra_id_' . $block['index'],
480
                (int) $block['extra_id']
481
            );
482
            $block['formElements']['hidExtraType'] = $this->form->addHidden(
483
                'block_extra_type_' . $block['index'],
484
                $block['extra_type']
485
            );
486
            $this->form->add(
487
                $this->getHiddenJsonField(
488
                    'block_extra_data_' . $block['index'],
489
                    $block['extra_data']
490
                )
491
            );
492
            $block['formElements']['hidExtraData'] = $this->form->getField('block_extra_data_' . $block['index']);
493
            $block['formElements']['hidPosition'] = $this->form->addHidden(
494
                'block_position_' . $block['index'],
495
                $block['position']
496
            );
497
            $block['formElements']['txtHTML'] = $this->form->addTextarea(
498
                'block_html_' . $block['index'],
499
                $block['html']
500
            ); // this is no editor; we'll add the editor in JS
501
502
            $this->positions[$block['position']]['blocks'][] = $block;
503
        }
504
505
        // redirect
506
        $redirectValue = 'none';
507
        if (isset($this->record['data']['internal_redirect']['page_id'])) {
508
            $redirectValue = 'internal';
509
        }
510
        if (isset($this->record['data']['external_redirect']['url'])) {
511
            $redirectValue = 'external';
512
        }
513
        $redirectValues = [
514
            ['value' => 'none', 'label' => \SpoonFilter::ucfirst(BL::lbl('None'))],
515
            [
516
                'value' => 'internal',
517
                'label' => \SpoonFilter::ucfirst(BL::lbl('InternalLink')),
518
                'variables' => ['isInternal' => true],
519
            ],
520
            [
521
                'value' => 'external',
522
                'label' => \SpoonFilter::ucfirst(BL::lbl('ExternalLink')),
523
                'variables' => ['isExternal' => true],
524
            ],
525
        ];
526
        $this->form->addRadiobutton('redirect', $redirectValues, $redirectValue);
527
        $this->form->addDropdown(
528
            'internal_redirect',
529
            BackendPagesModel::getPagesForDropdown(),
530
            ($redirectValue == 'internal') ? $this->record['data']['internal_redirect']['page_id'] : null
531
        );
532
        $this->form->addText(
533
            'external_redirect',
534
            ($redirectValue == 'external') ? urldecode($this->record['data']['external_redirect']['url']) : null,
535
            null,
536
            null,
537
            null,
538
            true
539
        );
540
541
        // page info
542
        $this->form->addCheckbox('navigation_title_overwrite', $this->record['navigation_title_overwrite']);
543
        $this->form->addText('navigation_title', $this->record['navigation_title']);
544
545
        if ($this->userCanSeeAndEditTags()) {
546
            // tags
547
            $this->form->addText(
548
                'tags',
549
                BackendTagsModel::getTags($this->url->getModule(), $this->id),
550
                null,
551
                'form-control js-tags-input',
552
                'error js-tags-input'
553
            )->setAttribute('aria-describedby', 'tags-info');
554
        }
555
556
        // a specific action
557
        $isAction = isset($this->record['data']['is_action']) && $this->record['data']['is_action'];
558
        $this->form->addCheckbox('is_action', $isAction);
559
560
        // extra
561
        $blockTypes = BackendPagesModel::getTypes();
562
        $this->form->addDropdown('extra_type', $blockTypes, key($blockTypes));
563
564
        // meta
565
        $this->meta = new BackendMeta($this->form, $this->record['meta_id'], 'title', true);
566
        $this->oldUrl = $this->meta->getUrl();
567
568
        // set callback for generating an unique URL
569
        $this->meta->setUrlCallback(
570
            'Backend\Modules\Pages\Engine\Model',
571
            'getUrl',
572
            [$this->record['id'], $this->record['parent_id'], $isAction]
573
        );
574
    }
575
576
    private function loadRevisions(): void
577
    {
578
        // create datagrid
579
        $this->dgRevisions = new BackendDataGridDatabase(
580
            BackendPagesModel::QUERY_BROWSE_REVISIONS,
581
            [
582
                 $this->id,
583
                 'archive',
584
                 BL::getWorkingLanguage(
585
                 ),
586
            ]
587
        );
588
589
        // hide columns
590
        $this->dgRevisions->setColumnsHidden(['id', 'revision_id']);
591
592
        // disable paging
593
        $this->dgRevisions->setPaging(false);
594
595
        // set headers
596
        $this->dgRevisions->setHeaderLabels(
597
            [
598
                 'user_id' => \SpoonFilter::ucfirst(BL::lbl('By')),
599
                 'edited_on' => \SpoonFilter::ucfirst(BL::lbl('LastEditedOn')),
600
            ]
601
        );
602
603
        // set functions
604
        $this->dgRevisions->setColumnFunction(
605
            [new BackendDataGridFunctions(), 'getUser'],
606
            ['[user_id]'],
607
            'user_id'
608
        );
609
        $this->dgRevisions->setColumnFunction(
610
            [new BackendDataGridFunctions(), 'getTimeAgo'],
611
            ['[edited_on]'],
612
            'edited_on'
613
        );
614
        $this->dgRevisions->setColumnFunction('htmlspecialchars', ['[title]'], 'title', false);
615
616
        // check if this action is allowed
617
        if (BackendAuthentication::isAllowedAction('Edit')) {
618
            // set column URLs
619
            $this->dgRevisions->setColumnURL(
620
                'title',
621
                BackendModel::createUrlForAction('Edit') . '&amp;id=[id]&amp;revision=[revision_id]'
622
            );
623
624
            // add use column
625
            $this->dgRevisions->addColumn(
626
                'use_revision',
627
                null,
628
                BL::lbl('UseThisVersion'),
629
                BackendModel::createUrlForAction('Edit') . '&amp;id=[id]&amp;revision=[revision_id]',
630
                BL::lbl('UseThisVersion')
631
            );
632
        }
633
    }
634
635
    protected function parse(): void
636
    {
637
        parent::parse();
638
639
        // set
640
        $this->record['url'] = $this->meta->getUrl();
641
        if ($this->id == 1) {
642
            $this->record['url'] = '';
643
        }
644
645
        // parse some variables
646
        $this->template->assign('item', $this->record);
647
        $this->template->assign('isGod', $this->isGod);
648
        $this->template->assign('templates', $this->templates);
649
        $this->template->assign('positions', $this->positions);
650
        $this->template->assign('extrasData', json_encode(BackendModel::recursiveHtmlspecialchars(BackendExtensionsModel::getExtrasData())));
651
        $this->template->assign('extrasById', json_encode($this->extras));
652
        $this->template->assign('prefixURL', rtrim(BackendPagesModel::getFullUrl($this->record['parent_id']), '/'));
653
        $this->template->assign('formErrors', (string) $this->form->getErrors());
654
        $this->template->assign('showTags', $this->userCanSeeAndEditTags());
655
        $this->template->assign('hreflangFields', $this->hreflangFields);
656
        $this->header->appendDetailToBreadcrumbs($this->record['title']);
657
658
        // init var
659
        $showDelete = true;
660
661
        // has children?
662
        if (BackendPagesModel::getFirstChildId($this->record['id']) !== false) {
663
            $showDelete = false;
664
        }
665
        if (!$this->record['delete_allowed']) {
666
            $showDelete = false;
667
        }
668
669
        // allowed?
670
        if (!BackendAuthentication::isAllowedAction('Delete', $this->getModule())) {
671
            $showDelete = false;
672
        }
673
674
        // show delete button
675
        $this->template->assign('allowPagesDelete', $showDelete);
676
677
        // assign template
678
        $this->template->assignArray($this->templates[$this->record['template_id']], 'template');
679
680
        // parse datagrids
681
        $this->template->assign(
682
            'revisions',
683
            ($this->dgRevisions->getNumResults() != 0) ? $this->dgRevisions->getContent() : false
684
        );
685
        $this->template->assign('drafts', ($this->dgDrafts->getNumResults() != 0) ? $this->dgDrafts->getContent() : false);
686
687
        // parse the tree
688
        $this->template->assign('tree', BackendPagesModel::getTreeHTML());
689
690
        // assign if profiles module is installed
691
        $this->template->assign('showAuthenticationTab', BackendModel::isModuleInstalled('Profiles'));
692
693
        $this->header->addJsData(
694
            'pages',
695
            'userTemplates',
696
            BackendPagesModel::loadUserTemplates()
697
        );
698
    }
699
700
    private function validateForm(): void
701
    {
702
        if (!$this->form->isSubmitted()) {
703
            return;
704
        }
705
706
        $status = $this->getRequest()->request->get('status');
707
        if (!in_array($status, ['active', 'draft'], true)) {
708
            $status = 'active';
709
        }
710
711
        $redirectValue = $this->form->getField('redirect')->getValue();
712
        if ($redirectValue === 'internal') {
713
            $this->form->getField('internal_redirect')->isFilled(
714
                BL::err('FieldIsRequired')
715
            );
716
        }
717
        if ($redirectValue === 'external') {
718
            $this->form->getField('external_redirect')->isURL(BL::err('InvalidURL'));
719
        }
720
721
        // set callback for generating an unique URL
722
        $this->meta->setUrlCallback(
723
            BackendPagesModel::class,
724
            'getUrl',
725
            [$this->record['id'], $this->record['parent_id'], $this->form->getField('is_action')->getChecked()]
726
        );
727
728
        $this->form->cleanupFields();
729
        $this->form->getField('title')->isFilled(BL::err('TitleIsRequired'));
730
        if ($this->form->getField('navigation_title_overwrite')->isChecked()) {
731
            $this->form->getField('navigation_title')->isFilled(BL::err('FieldIsRequired'));
732
        }
733
        $this->meta->validate();
734
735
        if ($this->form->getField('move_page')->isChecked()) {
736
            $this->form->getField('move_page_tree')->isFilled(BL::err('FieldIsRequired'));
737
            $this->form->getField('move_page_type')->isFilled(BL::err('FieldIsRequired'));
738
            $this->form->getField('move_page_reference_page')->isFilled(BL::err('FieldIsRequired'));
739
        }
740
741
        if (!$this->form->isCorrect()) {
742
            return;
743
        }
744
745
        $data = $this->buildPageData($redirectValue);
746
747
        $page = [
748
            'id' => $this->record['id'],
749
            'user_id' => BackendAuthentication::getUser()->getUserId(),
750
            'parent_id' => $this->record['parent_id'],
751
            'template_id' => (int) $this->form->getField('template_id')->getValue(),
752
            'meta_id' => $this->meta->save(),
753
            'language' => BL::getWorkingLanguage(),
754
            'type' => $this->record['type'],
755
            'title' => $this->form->getField('title')->getValue(),
756
            'navigation_title' => !empty($this->form->getField('navigation_title')->getValue())
757
                ? $this->form->getField('navigation_title')->getValue() : $this->form->getField('title')->getValue(),
758
            'navigation_title_overwrite' => $this->form->getField('navigation_title_overwrite')->isChecked(),
759
            'hidden' => $this->form->getField('hidden')->getValue(),
760
            'status' => $status,
761
            'publish_on' => BackendModel::getUTCDate(null, $this->record['publish_on']),
762
            'created_on' => BackendModel::getUTCDate(null, $this->record['created_on']),
763
            'edited_on' => BackendModel::getUTCDate(),
764
            'allow_move' => $this->record['allow_move'],
765
            'allow_children' => $this->record['allow_children'],
766
            'allow_edit' => $this->record['allow_edit'],
767
            'allow_delete' => $this->record['allow_delete'],
768
            'sequence' => $this->record['sequence'],
769
            'data' => serialize($data),
770
        ];
771
772
        $page = $this->changePagePermissions($page);
773
774
        if (empty($page['navigation_title'])) {
775
            $page['navigation_title'] = $page['title'];
776
        }
777
778
        // update page, store the revision id, we need it when building the blocks
779
        $page['revision_id'] = BackendPagesModel::update($page);
780
781
        $this->insertBlocks($page['revision_id']);
782
783
        $this->saveTags($page['id']);
784
785
        $cacheShouldBeUpdated = !(
786
            $this->record['title'] === $page['title']
787
            && $this->record['navigation_title'] === $page['navigation_title']
788
            && $this->record['navigation_title_overwrite'] == $page['navigation_title_overwrite'] // can be 0 or false
789
            && $this->record['hidden'] == $page['hidden'] // can be 0 or false
790
            && $this->meta->getUrl() === $this->oldUrl
791
        );
792
793
        // build cache
794
        if ($cacheShouldBeUpdated) {
795
            BackendPagesModel::buildCache(BL::getWorkingLanguage());
796
        }
797
798
        if ($page['status'] === 'draft') {
799
            $this->redirect(
800
                BackendModel::createUrlForAction('Edit') . '&id=' . $page['id']
801
                . '&report=saved-as-draft&var=' . rawurlencode($page['title']) . '&highlight=row-' . $page['id']
802
                . '&draft=' . $page['revision_id']
803
            );
804
805
            return;
806
        }
807
808
        $this->movePage($page);
809
        $this->saveSearchIndex($data['remove_from_search_index'] || $redirectValue !== 'none', $page);
810
811
        $this->redirect(
812
            BackendModel::createUrlForAction('Edit') . '&id=' . $page['id'] . '&report=edited&var='
813
            . rawurlencode($page['title']) . '&highlight=row-' . $page['id']
814
        );
815
    }
816
817
    private function changePagePermissions(array $page): array
818
    {
819
        if (!$this->isGod) {
820
            return $page;
821
        }
822
823
        $page['allow_move'] = BackendPagesModel::isForbiddenToMove($this->id) ? false : in_array(
824
            'move',
825
            (array) $this->form->getField('allow')->getValue(),
826
            true
827
        );
828
        $page['allow_children'] = BackendPagesModel::isForbiddenToHaveChildren($this->id) ? false : in_array(
829
            'children',
830
            (array) $this->form->getField('allow')->getValue(),
831
            true
832
        );
833
        $page['allow_edit'] = in_array(
834
            'edit',
835
            (array) $this->form->getField('allow')->getValue(),
836
            true
837
        );
838
        $page['allow_delete'] = BackendPagesModel::isForbiddenToDelete($this->id) ? false : in_array(
839
            'delete',
840
            (array) $this->form->getField('allow')->getValue(),
841
            true
842
        );
843
844
        return $page;
845
    }
846
847
    private function movePage(array $page): void
848
    {
849
        if (!$page['allow_move'] || !$this->form->getField('move_page')->isChecked()) {
850
            return;
851
        }
852
853
        BackendPagesModel::move(
854
            $page['id'],
855
            (int) $this->form->getField('move_page_reference_page')->getValue(),
856
            $this->form->getField('move_page_type')->getValue(),
857
            $this->form->getField('move_page_tree')->getValue()
858
        );
859
        BackendPagesModel::buildCache(BL::getWorkingLanguage());
860
    }
861
862
    private function buildPageData(string $redirectValue): array
863
    {
864
        $data = [];
865
        $templateId = (int) $this->form->getField('template_id')->getValue();
866
867
        if ($this->form->getField('is_action')->isChecked()) {
868
            $data['is_action'] = true;
869
        }
870
        if ($redirectValue === 'internal') {
871
            $data['internal_redirect'] = [
872
                'page_id' => $this->form->getField('internal_redirect')->getValue(),
873
                'code' => Response::HTTP_TEMPORARY_REDIRECT,
874
            ];
875
        }
876
        if ($redirectValue === 'external') {
877
            $data['external_redirect'] = [
878
                'url' => BackendPagesModel::getEncodedRedirectUrl(
879
                    $this->form->getField('external_redirect')->getValue()
880
                ),
881
                'code' => Response::HTTP_TEMPORARY_REDIRECT,
882
            ];
883
        }
884
        if (array_key_exists('image', $this->templates[$templateId]['data'])) {
885
            $data['image'] = $this->getImage($this->templates[$templateId]['data']['image']);
886
        }
887
888
        $data['auth_required'] = false;
889
        if (BackendModel::isModuleInstalled('Profiles') && $this->form->getField('auth_required')->isChecked()) {
890
            $data['auth_required'] = true;
891
            // get all groups and parse them in key value pair
892
            $groupItems = BackendProfilesModel::getGroups();
893
894
            if (!empty($groupItems)) {
895
                $data['auth_groups'] = $this->form->getField('auth_groups')->getValue();
896
            }
897
        }
898
899
        $data['remove_from_search_index'] = false;
900
        if (BackendModel::isModuleInstalled('Profiles')
901
            && $this->form->getField('remove_from_search_index')->isChecked()
902
            && $this->form->getField('auth_required')->isChecked()) {
903
            $data['remove_from_search_index'] = true;
904
        }
905
906
        // just execute if the site is multi-language
907
        if ($this->getContainer()->getParameter('site.multilanguage')) {
908
            // loop active languages
909
            foreach (BL::getActiveLanguages() as $language) {
910
                if ($language != BL::getWorkingLanguage()
911
                    && $this->form->getfield('hreflang_' . $language)->isFilled()) {
912
                    $data['hreflang_' . $language] = $this->form->getfield('hreflang_' . $language)->getValue();
913
                }
914
            }
915
        }
916
917
        // link class
918
        $data['link_class'] = $this->record['data']['link_class'] ?? null;
919
        if ($this->isGod) {
920
            $data['link_class'] = $this->form->getField('link_class')->getValue();
921
        }
922
923
        return $data;
924
    }
925
926
    private function insertBlocks(int $revisionId): void
927
    {
928
        $possiblePositions = $this->templates[$this->form->getField('template_id')->getValue()]['data']['names'];
929
        foreach ($this->blocksContent as $i => $block) {
930
            $this->blocksContent[$i]['revision_id'] = $revisionId;
931
932
            // validate blocks, only save blocks for valid positions
933
            if (!in_array($block['position'], $possiblePositions, true)) {
934
                unset($this->blocksContent[$i]);
935
            }
936
        }
937
938
        BackendPagesModel::insertBlocks($this->blocksContent);
939
    }
940
941
    private function saveTags(int $pageId): void
942
    {
943
        if (!$this->userCanSeeAndEditTags()) {
944
            return;
945
        }
946
947
        BackendTagsModel::saveTags(
948
            $pageId,
949
            $this->form->getField('tags')->getValue(),
950
            $this->url->getModule()
951
        );
952
    }
953
954
    private function saveSearchIndex(bool $removeFromSearchIndex, array $page): void
955
    {
956
        if ($removeFromSearchIndex) {
957
            BackendSearchModel::removeIndex(
958
                $this->getModule(),
959
                $page['id']
960
            );
961
962
            return;
963
        }
964
965
        $searchText = '';
966
        foreach ($this->blocksContent as $block) {
967
            $searchText .= ' ' . $block['html'];
968
        }
969
970
        BackendSearchModel::saveIndex(
971
            $this->getModule(),
972
            $page['id'],
973
            ['title' => $page['title'], 'text' => $searchText]
974
        );
975
    }
976
977
    private function getImage(bool $allowImage): ?string
978
    {
979
        $imageFilename = array_key_exists('image', (array) $this->record['data']) ? $this->record['data']['image'] : null;
980
981
        if (!$this->form->getField('image')->isFilled() && !$this->form->getField('remove_image')->isChecked()) {
982
            return $imageFilename;
983
        }
984
985
        $imagePath = FRONTEND_FILES_PATH . '/Pages/images';
986
987
        // delete the current image
988
        $this->get(Thumbnails::class)->delete($imagePath, (string) $imageFilename);
989
990
        if (!$allowImage
991
            || ($this->form->getField('remove_image')->isChecked() && !$this->form->getField('image')->isFilled())
992
        ) {
993
            return null;
994
        }
995
996
        $imageFilename = $this->meta->getUrl() . '_' . time() . '.' . $this->form->getField('image')->getExtension();
997
        $this->form->getField('image')->generateThumbnails($imagePath, $imageFilename);
998
999
        return $imageFilename;
1000
    }
1001
1002
    private function userCanSeeAndEditTags(): bool
1003
    {
1004
        return Authentication::isAllowedAction('Edit', 'Tags') && Authentication::isAllowedAction('GetAllTags', 'Tags');
1005
    }
1006
1007
    private function loadDeleteForm(): void
1008
    {
1009
        $deleteForm = $this->createForm(
1010
            DeleteType::class,
1011
            ['id' => $this->record['id']],
1012
            ['module' => $this->getModule()]
1013
        );
1014
        $this->template->assign('deleteForm', $deleteForm->createView());
1015
    }
1016
1017
    private function getHiddenJsonField(string $name, ?string $json): SpoonFormHidden
1018
    {
1019
        return new class($name, htmlspecialchars($json)) extends SpoonFormHidden {
0 ignored issues
show
It seems like $json can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1019
        return new class($name, htmlspecialchars(/** @scrutinizer ignore-type */ $json)) extends SpoonFormHidden {
Loading history...
1020
            public function getValue($allowHTML = null)
1021
            {
1022
                return parent::getValue(true);
1023
            }
1024
        };
1025
    }
1026
}
1027