Edit::validateForm()   F
last analyzed

Complexity

Conditions 17
Paths 2593

Size

Total Lines 114
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 75
c 1
b 0
f 0
nc 2593
nop 0
dl 0
loc 114
ccs 0
cts 92
cp 0
crap 306
rs 1.0499

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\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);
0 ignored issues
show
Documentation Bug introduced by
It seems like Backend\Modules\Pages\En...e\Model::get($this->id) can also be of type false. However, the property $record is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$page was never initialized. Although not strictly required by PHP, it is generally a good practice to add $page = array(); before regardless.
Loading history...
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),
0 ignored issues
show
Bug introduced by
It seems like Backend\Modules\Tags\Eng...getModule(), $this->id) can also be of type array; however, parameter $value of Common\Core\Form::addText() 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

549
                /** @scrutinizer ignore-type */ BackendTagsModel::getTags($this->url->getModule(), $this->id),
Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The class Backend\Core\Engine\Meta has been deprecated: This class will be removed when all modules run on doctrine and will be replaced with the meta entity ( Ignorable by Annotation )

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

565
        $this->meta = /** @scrutinizer ignore-deprecated */ new BackendMeta($this->form, $this->record['meta_id'], 'title', true);
Loading history...
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
Bug introduced by
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