GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

AdministrationPage   F
last analyzed

Complexity

Total Complexity 178

Size/Duplication

Total Lines 1430
Duplicated Lines 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
eloc 469
c 15
b 0
f 0
dl 0
loc 1430
rs 2
wmc 178

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getContext() 0 3 1
A setPageType() 0 3 3
F build() 0 154 13
A insertBreadcrumbs() 0 22 5
B __findActiveNavigationGroup() 0 17 8
A insertAction() 0 19 3
A appendSubheading() 0 13 5
A __appendBodyId() 0 19 2
A addTimestampValidationPageAlert() 0 39 4
A __switchboard() 0 20 6
B __appendBodyClass() 0 29 8
A insertDrawer() 0 16 3
A pageAlert() 0 13 4
A getNavigationArray() 0 7 2
A __construct() 0 5 1
A appendUserLinks() 0 18 1
A sortAlerts() 0 14 6
A createParentNavItem() 0 11 3
A setBodyClass() 0 5 4
B buildSectionNavigation() 0 66 7
A __navigationFindGroupIndex() 0 9 3
B buildExtensionsNavigation() 0 54 11
B doesAuthorHaveAccess() 0 15 10
D canAccessPage() 0 75 18
A appendAlert() 0 27 4
D appendNavigation() 0 79 18
A __buildNavigation() 0 39 3
A view() 0 3 1
B buildXmlNavigation() 0 45 9
A action() 0 3 1
B createChildNavItem() 0 17 7
A generate() 0 38 4

How to fix   Complexity   

Complex Class

Complex classes like AdministrationPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AdministrationPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package toolkit
5
 */
6
/**
7
 * The AdministrationPage class represents a Symphony backend page.
8
 * It extends the HTMLPage class and unlike the Frontend, is generated
9
 * using a number XMLElement objects. Instances of this class override
10
 * the view, switchboard and action functions to construct the page. These
11
 * functions act as pseudo MVC, with the switchboard being controller,
12
 * and the view/action being the view.
13
 */
14
15
class AdministrationPage extends HTMLPage
16
{
17
    /**
18
     * An array of `Alert` objects used to display page level
19
     * messages to Symphony backend users one by one. Prior to Symphony 2.3
20
     * this variable only held a single `Alert` object.
21
     * @var array
22
     */
23
    public $Alert = array();
24
25
    /**
26
     * As the name suggests, a `<div>` that holds the following `$Header`,
27
     * `$Contents` and `$Footer`.
28
     * @var XMLElement
29
     */
30
    public $Wrapper = null;
31
32
    /**
33
     * A `<div>` that contains the header of a Symphony backend page, which
34
     * typically contains the Site title and the navigation.
35
     * @var XMLElement
36
     */
37
    public $Header = null;
38
39
    /**
40
     * A `<div>` that contains the breadcrumbs, the page title and some contextual
41
     * actions (e.g. "Create new").
42
     * @since Symphony 2.3
43
     * @var XMLElement
44
     */
45
    public $Context = null;
46
47
    /**
48
     * An object that stores the markup for the breadcrumbs and is only used
49
     * internally.
50
     * @since Symphony 2.3
51
     * @var XMLElement
52
     */
53
    public $Breadcrumbs = null;
54
55
    /**
56
     * An array of Drawer widgets for the current page
57
     * @since Symphony 2.3
58
     * @var array
59
     */
60
    public $Drawer = array();
61
62
    /**
63
     * A `<div>` that contains the content of a Symphony backend page.
64
     * @var XMLElement
65
     */
66
    public $Contents = null;
67
68
    /**
69
     * An associative array of the navigation where the key is the group
70
     * index, and the value is an associative array of 'name', 'index' and
71
     * 'children'. Name is the name of the this group, index is the same as
72
     * the key and children is an associative array of navigation items containing
73
     * the keys 'link', 'name' and 'visible'. In Symphony, all navigation items
74
     * are contained within a group, and the group has no 'default' link, therefore
75
     * it is up to the children to provide the link to pages. This link should be
76
     * relative to the Symphony path, although it is possible to provide an
77
     * absolute link by providing a key, 'relative' with the value false.
78
     * @var array
79
     */
80
    public $_navigation = array();
81
82
    /**
83
     *  An associative array describing this pages context. This
84
     *  can include the section handle, the current entry_id, the page
85
     *  name and any flags such as 'saved' or 'created'. This variable
86
     *  often provided in delegates so extensions can manipulate based
87
     *  off the current context or add new keys.
88
     * @var array
89
     */
90
    public $_context = null;
91
92
    /**
93
     * The class attribute of the `<body>` element for this page. Defaults
94
     * to an empty string
95
     * @var string
96
     */
97
    private $_body_class = '';
98
99
    /**
100
     * Constructor calls the parent constructor to set up
101
     * the basic HTML, Head and Body `XMLElement`'s. This function
102
     * also sets the `XMLElement` element style to be HTML, instead of XML
103
     */
104
    public function __construct()
105
    {
106
        parent::__construct();
107
108
        $this->Html->setElementStyle('html');
109
    }
110
111
    /**
112
     * Specifies the type of page that being created. This is used to
113
     * trigger various styling hooks. If your page is mainly a form,
114
     * pass 'form' as the parameter, if it's displaying a single entry,
115
     * pass 'single'. If any other parameter is passed, the 'index'
116
     * styling will be applied.
117
     *
118
     * @param string $type
119
     *  Accepts 'form' or 'single', any other `$type` will trigger 'index'
120
     *  styling.
121
     */
122
    public function setPageType($type = 'form')
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$type" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$type"; expected 0 but found 1
Loading history...
123
    {
124
        $this->setBodyClass($type == 'form' || $type == 'single' ? 'single' : 'index');
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
125
    }
126
127
    /**
128
     * Setter function to set the class attribute on the `<body>` element.
129
     * This function will respect any previous classes that have been added
130
     * to this `<body>`
131
     *
132
     * @param string $class
133
     *  The string of the classname, multiple classes can be specified by
134
     *  uses a space separator
135
     */
136
    public function setBodyClass($class)
137
    {
138
        // Prevents duplicate "index" classes
139
        if (!isset($this->_context['page']) || $this->_context['page'] !== 'index' || $class !== 'index') {
140
            $this->_body_class .= $class;
141
        }
142
    }
143
144
    /**
145
     * Accessor for `$this->_context` which includes contextual information
146
     * about the current page such as the class, file location or page root.
147
     * This information varies depending on if the page is provided by an
148
     * extension, is for the publish area, is the login page or any other page
149
     *
150
     * @since Symphony 2.3
151
     * @return array
152
     */
153
    public function getContext()
154
    {
155
        return $this->_context;
156
    }
157
158
    /**
159
     * Given a `$message` and an optional `$type`, this function will
160
     * add an Alert instance into this page's `$this->Alert` property.
161
     * Since Symphony 2.3, there may be more than one `Alert` per page.
162
     * Unless the Alert is an Error, it is required the `$message` be
163
     * passed to this function.
164
     *
165
     * @param string $message
166
     *  The message to display to users
167
     * @param string $type
168
     *  An Alert constant, being `Alert::NOTICE`, `Alert::ERROR` or
169
     *  `Alert::SUCCESS`. The differing types will show the error
170
     *  in a different style in the backend. If omitted, this defaults
171
     *  to `Alert::NOTICE`.
172
     * @throws Exception
173
     */
174
    public function pageAlert($message = null, $type = Alert::NOTICE)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$message" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$message"; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between argument "$type" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$type"; expected 0 but found 1
Loading history...
175
    {
176
        if (is_null($message) && $type == Alert::ERROR) {
177
            $message = __('There was a problem rendering this page. Please check the activity log for more details.');
178
        } else {
179
            $message = __($message);
180
        }
181
182
        if (strlen(trim($message)) == 0) {
183
            throw new Exception(__('A message must be supplied unless the alert is of type Alert::ERROR'));
184
        }
185
186
        $this->Alert[] = new Alert($message, $type);
187
    }
188
189
    /**
190
     * Appends the heading of this Symphony page to the Context element.
191
     * Action buttons can be provided (e.g. "Create new") as second parameter.
192
     *
193
     * @since Symphony 2.3
194
     * @param string $value
195
     *  The heading text
196
     * @param array|XMLElement|string $actions
197
     *  Some contextual actions to append to the heading, they can be provided as
198
     *  an array of XMLElements or strings. Traditionally Symphony uses this to append
199
     *  a "Create new" link to the Context div.
200
     */
201
    public function appendSubheading($value, $actions = null)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$actions" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$actions"; expected 0 but found 1
Loading history...
202
    {
203
        if (!is_array($actions) && $actions) { // Backward compatibility
204
            $actions = array($actions);
205
        }
206
207
        if (!empty($actions)) {
208
            foreach ($actions as $a) {
209
                $this->insertAction($a);
210
            }
211
        }
212
213
        $this->Breadcrumbs->appendChild(new XMLElement('h2', $value, array('role' => 'heading', 'id' => 'symphony-subheading')));
214
    }
215
216
    /**
217
     * This function allows a user to insert an Action button to the page.
218
     * It accepts an `XMLElement` (which should be of the `Anchor` type),
219
     * an optional parameter `$prepend`, which when `true` will add this
220
     * action before any existing actions.
221
     *
222
     * @since Symphony 2.3
223
     * @see core.Widget#Anchor
224
     * @param XMLElement $action
225
     *  An Anchor element to add to the top of the page.
226
     * @param boolean $append
227
     *  If true, this will add the `$action` after existing actions, otherwise
228
     *  it will be added before existing actions. By default this is `true`,
229
     *  which will add the `$action` after current actions.
230
     */
231
    public function insertAction(XMLElement $action, $append = true)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$append" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$append"; expected 0 but found 1
Loading history...
232
    {
233
        $actions = $this->Context->getChildrenByName('ul');
234
235
        // Actions haven't be added yet, create the element
236
        if (empty($actions)) {
237
            $ul = new XMLElement('ul', null, array('class' => 'actions'));
238
            $this->Context->appendChild($ul);
239
        } else {
240
            $ul = current($actions);
241
            $this->Context->replaceChildAt(1, $ul);
242
        }
243
244
        $li = new XMLElement('li', $action);
245
246
        if ($append) {
247
            $ul->prependChild($li);
248
        } else {
249
            $ul->appendChild($li);
250
        }
251
    }
252
253
    /**
254
     * Allows developers to specify a list of nav items that build the
255
     * path to the current page or, in jargon, "breadcrumbs".
256
     *
257
     * @since Symphony 2.3
258
     * @param array $values
259
     *  An array of `XMLElement`'s or strings that compose the path. If breadcrumbs
260
     *  already exist, any new item will be appended to the rightmost part of the
261
     *  path.
262
     */
263
    public function insertBreadcrumbs(array $values)
264
    {
265
        if (empty($values)) {
266
            return;
267
        }
268
269
        if ($this->Breadcrumbs instanceof XMLElement && count($this->Breadcrumbs->getChildrenByName('nav')) === 1) {
270
            $nav = $this->Breadcrumbs->getChildrenByName('nav');
271
            $nav = $nav[0];
272
273
            $p = $nav->getChild(0);
274
        } else {
275
            $p = new XMLElement('p');
276
            $nav = new XMLElement('nav');
277
            $nav->appendChild($p);
278
279
            $this->Breadcrumbs->prependChild($nav);
280
        }
281
282
        foreach ($values as $v) {
283
            $p->appendChild($v);
284
            $p->appendChild(new XMLElement('span', '&#8250;', array('class' => 'sep')));
285
        }
286
    }
287
288
    /**
289
     * Allows a Drawer element to added to the backend page in one of three
290
     * positions, `horizontal`, `vertical-left` or `vertical-right`. The button
291
     * to trigger the visibility of the drawer will be added after existing
292
     * actions by default.
293
     *
294
     * @since Symphony 2.3
295
     * @see core.Widget#Drawer
296
     * @param XMLElement $drawer
297
     *  An XMLElement representing the drawer, use `Widget::Drawer` to construct
298
     * @param string $position
299
     *  Where `$position` can be `horizontal`, `vertical-left` or
300
     *  `vertical-right`. Defaults to `horizontal`.
301
     * @param string $button
302
     *  If not passed, a button to open/close the drawer will not be added
303
     *  to the interface. Accepts 'prepend' or 'append' values, which will
304
     *  add the button before or after existing buttons. Defaults to `prepend`.
305
     *  If any other value is passed, no button will be added.
306
     * @throws InvalidArgumentException
307
     */
308
    public function insertDrawer(XMLElement $drawer, $position = 'horizontal', $button = 'append')
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$position" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$position"; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between argument "$button" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$button"; expected 0 but found 1
Loading history...
309
    {
310
        $drawer->addClass($position);
311
        $drawer->setAttribute('data-position', $position);
312
        $drawer->setAttribute('role', 'complementary');
313
        $this->Drawer[$position][] = $drawer;
314
315
        if (in_array($button, array('prepend', 'append'))) {
316
            $this->insertAction(
317
                Widget::Anchor(
318
                    $drawer->getAttribute('data-label'),
319
                    '#' . $drawer->getAttribute('id'),
320
                    null,
321
                    'button drawer ' . $position
322
                ),
323
                ($button === 'append' ? true : false)
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
324
            );
325
        }
326
    }
327
328
    /**
329
     * This function initialises a lot of the basic elements that make up a Symphony
330
     * backend page such as the default stylesheets and scripts, the navigation and
331
     * the footer. Any alerts are also appended by this function. `view()` is called to
332
     * build the actual content of the page. The `InitialiseAdminPageHead` delegate
333
     * allows extensions to add elements to the `<head>`. The `CanAccessPage` delegate
334
     * allows extensions to restrict access to pages.
335
     *
336
     * @see view()
337
     * @uses InitialiseAdminPageHead
338
     * @uses CanAccessPage
339
     * @param array $context
340
     *  An associative array describing this pages context. This
341
     *  can include the section handle, the current entry_id, the page
342
     *  name and any flags such as 'saved' or 'created'. This list is not exhaustive
343
     *  and extensions can add their own keys to the array.
344
     * @throws InvalidArgumentException
345
     * @throws SymphonyErrorPage
346
     */
347
    public function build(array $context = array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$context" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$context"; expected 0 but found 1
Loading history...
348
    {
349
        $this->_context = $context;
350
351
        if (!$this->canAccessPage()) {
352
            Administration::instance()->throwCustomError(
353
                __('You are not authorised to access this page.'),
354
                __('Access Denied'),
355
                Page::HTTP_STATUS_UNAUTHORIZED
356
            );
357
        }
358
359
        $this->Html->setDTD('<!DOCTYPE html>');
360
        $this->Html->setAttribute('lang', Lang::get());
361
        $this->addElementToHead(new XMLElement('meta', null, array('charset' => 'UTF-8')), 0);
362
        $this->addElementToHead(new XMLElement('meta', null, array('http-equiv' => 'X-UA-Compatible', 'content' => 'IE=edge,chrome=1')), 1);
363
        $this->addElementToHead(new XMLElement('meta', null, array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1')), 2);
364
365
        // Add styles
366
        $this->addStylesheetToHead(ASSETS_URL . '/css/symphony.min.css', 'screen', 2, false);
0 ignored issues
show
Bug introduced by
The constant ASSETS_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
367
368
        // Calculate timezone offset from UTC
369
        $timezone = new DateTimeZone(DateTimeObj::getSetting('timezone'));
0 ignored issues
show
Bug introduced by
It seems like DateTimeObj::getSetting('timezone') can also be of type array; however, parameter $timezone of DateTimeZone::__construct() 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

369
        $timezone = new DateTimeZone(/** @scrutinizer ignore-type */ DateTimeObj::getSetting('timezone'));
Loading history...
370
        $datetime = new DateTime('now', $timezone);
371
        $timezoneOffset = intval($timezone->getOffset($datetime)) / 60;
372
373
        // Add scripts
374
        $environment = array(
375
376
            'root'     => URL,
0 ignored issues
show
Bug introduced by
The constant URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
377
            'symphony' => SYMPHONY_URL,
0 ignored issues
show
Bug introduced by
The constant SYMPHONY_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
378
            'path'     => '/' . Symphony::Configuration()->get('admin-path', 'symphony'),
379
            'route'    => getCurrentPage(),
380
            'version'  => Symphony::Configuration()->get('version', 'symphony'),
381
            'lang'     => Lang::get(),
382
            'user'     => array(
383
384
                'fullname' => Symphony::Author()->getFullName(),
385
                'name'     => Symphony::Author()->get('first_name'),
386
                'type'     => Symphony::Author()->get('user_type'),
387
                'id'       => Symphony::Author()->get('id')
388
            ),
389
            'datetime' => array(
390
391
                'formats'         => DateTimeObj::getDateFormatMappings(),
392
                'timezone-offset' => $timezoneOffset
393
            ),
394
            'env' => array_merge(
395
396
                array('page-namespace' => Symphony::getPageNamespace()),
397
                $this->_context
398
            )
399
        );
400
401
        $envJsonOptions = 0;
402
        // Those are PHP 5.4+
403
        if (defined('JSON_UNESCAPED_SLASHES') && defined('JSON_UNESCAPED_UNICODE')) {
404
            $envJsonOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
405
        }
406
407
        $this->addElementToHead(
408
            new XMLElement('script', json_encode($environment, $envJsonOptions), array(
409
                'type' => 'application/json',
410
                'id' => 'environment'
411
            )),
412
            4
413
        );
414
415
        $this->addScriptToHead(ASSETS_URL . '/js/symphony.min.js', 6, false);
416
417
        // Initialise page containers
418
        $this->Wrapper = new XMLElement('div', null, array('id' => 'wrapper'));
419
        $this->Header = new XMLElement('header', null, array('id' => 'header'));
420
        $this->Context = new XMLElement('div', null, array('id' => 'context'));
421
        $this->Breadcrumbs = new XMLElement('div', null, array('id' => 'breadcrumbs'));
422
        $this->Contents = new XMLElement('div', null, array('id' => 'contents', 'role' => 'main'));
423
        $this->Form = Widget::Form(Administration::instance()->getCurrentPageURL(), 'post', null, null, array('role' => 'form'));
424
425
        /**
426
         * Allows developers to insert items into the page HEAD. Use
427
         * `Administration::instance()->Page` for access to the page object.
428
         *
429
         * @since In Symphony 2.3.2 this delegate was renamed from
430
         *  `InitaliseAdminPageHead` to the correct spelling of
431
         *  `InitialiseAdminPageHead`. The old delegate is supported
432
         *  until Symphony 3.0
433
         *
434
         * @delegate InitialiseAdminPageHead
435
         * @param string $context
436
         *  '/backend/'
437
         */
438
        Symphony::ExtensionManager()->notifyMembers('InitialiseAdminPageHead', '/backend/');
439
        Symphony::ExtensionManager()->notifyMembers('InitaliseAdminPageHead', '/backend/');
440
441
        $this->addHeaderToPage('Content-Type', 'text/html; charset=UTF-8');
442
        $this->addHeaderToPage('Cache-Control', 'no-cache, must-revalidate, max-age=0');
443
        $this->addHeaderToPage('Expires', 'Mon, 12 Dec 1982 06:14:00 GMT');
444
        $this->addHeaderToPage('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
445
        $this->addHeaderToPage('Pragma', 'no-cache');
446
447
        // If not set by another extension, lock down the backend
448
        if (!array_key_exists('x-frame-options', $this->headers())) {
449
            $this->addHeaderToPage('X-Frame-Options', 'SAMEORIGIN');
450
        }
451
452
        if (!array_key_exists('x-content-type-options', $this->headers())) {
453
            $this->addHeaderToPage('X-Content-Type-Options', 'nosniff');
454
        }
455
456
        if (!array_key_exists('x-xss-protection', $this->headers())) {
457
            $this->addHeaderToPage('X-XSS-Protection', '1; mode=block');
458
        }
459
460
        if (!array_key_exists('referrer-policy', $this->headers())) {
461
            $this->addHeaderToPage('Referrer-Policy', 'same-origin');
462
        }
463
464
        if (isset($_REQUEST['action'])) {
465
            $this->action();
466
            Symphony::Profiler()->sample('Page action run', PROFILE_LAP);
0 ignored issues
show
Bug introduced by
The constant PROFILE_LAP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
467
        }
468
469
        $h1 = new XMLElement('h1');
470
        $h1->appendChild(Widget::Anchor(Symphony::Configuration()->get('sitename', 'general'), rtrim(URL, '/') . '/'));
0 ignored issues
show
Bug introduced by
It seems like Symphony::Configuration(...('sitename', 'general') can also be of type array; however, parameter $value of Widget::Anchor() 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

470
        $h1->appendChild(Widget::Anchor(/** @scrutinizer ignore-type */ Symphony::Configuration()->get('sitename', 'general'), rtrim(URL, '/') . '/'));
Loading history...
471
        $this->Header->appendChild($h1);
472
473
        $this->appendUserLinks();
474
        $this->appendNavigation();
475
476
        // Add Breadcrumbs
477
        $this->Context->prependChild($this->Breadcrumbs);
478
        $this->Contents->appendChild($this->Form);
479
480
        // Validate date time config
481
        $dateFormat = defined('__SYM_DATE_FORMAT__') ? __SYM_DATE_FORMAT__ : null;
0 ignored issues
show
Bug introduced by
The constant __SYM_DATE_FORMAT__ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
482
        if (empty($dateFormat)) {
483
            $this->pageAlert(
484
                __('Your <code>%s</code> file does not define a date format', array(basename(CONFIG))),
0 ignored issues
show
Bug introduced by
The constant CONFIG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
485
                Alert::NOTICE
486
            );
487
        }
0 ignored issues
show
Coding Style introduced by
No blank line found after control structure
Loading history...
488
        $timeFormat = defined('__SYM_TIME_FORMAT__') ? __SYM_TIME_FORMAT__ : null;
0 ignored issues
show
Bug introduced by
The constant __SYM_TIME_FORMAT__ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
489
        if (empty($timeFormat)) {
490
            $this->pageAlert(
491
                __('Your <code>%s</code> file does not define a time format.', array(basename(CONFIG))),
492
                Alert::NOTICE
493
            );
494
        }
495
496
        $this->view();
497
498
        $this->appendAlert();
499
500
        Symphony::Profiler()->sample('Page content created', PROFILE_LAP);
501
    }
502
503
    /**
504
     * Checks the current Symphony Author can access the current page.
505
     * This check uses the `ASSETS . /xml/navigation.xml` file to determine
506
     * if the current page (or the current page namespace) can be viewed
507
     * by the currently logged in Author.
508
     *
509
     * @since Symphony 2.7.0
510
     * It fires a delegate, CanAccessPage, to allow extensions to restrict access
511
     * to the current page
512
     *
513
     * @uses CanAccessPage
514
     *
515
     * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
516
     * @return boolean
517
     *  true if the Author can access the current page, false otherwise
518
     */
519
    public function canAccessPage()
520
    {
521
        $nav = $this->getNavigationArray();
522
        $page = '/' . trim(getCurrentPage(), '/') . '/';
523
524
        $page_limit = 'author';
525
526
        foreach ($nav as $item) {
527
            if (
0 ignored issues
show
Coding Style introduced by
Expected 0 spaces after opening bracket; newline found
Loading history...
528
                // If page directly matches one of the children
529
                General::in_array_multi($page, $item['children'])
530
                // If the page namespace matches one of the children (this will usually drop query
531
                // string parameters such as /edit/1/)
532
                || General::in_array_multi(Symphony::getPageNamespace() . '/', $item['children'])
533
            ) {
534
                if (is_array($item['children'])) {
535
                    foreach ($item['children'] as $c) {
536
                        if ($c['link'] === $page && isset($c['limit'])) {
537
                            $page_limit = $c['limit'];
538
                            // TODO: break out of the loop here in Symphony 3.0.0
539
                        }
540
                    }
541
                }
542
543
                if (isset($item['limit']) && $page_limit !== 'primary') {
544
                    if ($page_limit === 'author' && $item['limit'] === 'developer') {
545
                        $page_limit = 'developer';
546
                    }
547
                }
548
            } elseif (isset($item['link']) && $page === $item['link'] && isset($item['limit'])) {
549
                $page_limit = $item['limit'];
550
            }
551
        }
552
553
        $hasAccess = $this->doesAuthorHaveAccess($page_limit);
554
555
        if ($hasAccess) {
556
            $page_context = $this->getContext();
557
            $section_handle = !isset($page_context['section_handle']) ? null : $page_context['section_handle'];
558
            /**
559
             * Immediately after the core access rules allowed access to this page
560
             * (i.e. not called if the core rules denied it).
561
             * Extension developers must only further restrict access to it.
562
             * Extension developers must also take care of checking the current value
563
             * of the allowed parameter in order to prevent conflicts with other extensions.
564
             * `$context['allowed'] = $context['allowed'] && customLogic();`
565
             *
566
             * @delegate CanAccessPage
567
             * @since Symphony 2.7.0
568
             * @see doesAuthorHaveAccess()
569
             * @param string $context
570
             *  '/backend/'
571
             * @param bool $allowed
572
             *  A flag to further restrict access to the page, passed by reference
573
             * @param string $page_limit
574
             *  The computed page limit for the current page
575
             * @param string $page_url
576
             *  The computed page url for the current page
577
             * @param int $section.id
578
             *  The id of the section for this url
579
             * @param string $section.handle
580
             *  The handle of the section for this url
581
             */
582
            Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array(
583
                'allowed' => &$hasAccess,
584
                'page_limit' => $page_limit,
585
                'page_url' => $page,
586
                'section' => array(
587
                    'id' => !$section_handle ? 0 : SectionManager::fetchIDFromHandle($section_handle),
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
588
                    'handle' => $section_handle
589
                ),
590
            ));
591
        }
592
593
        return $hasAccess;
594
    }
595
596
    /**
597
     * Given the limit of the current navigation item or page, this function
598
     * returns if the current Author can access that item or not.
599
     *
600
     * @since Symphony 2.5.1
601
     * @param string $item_limit
602
     * @return boolean
603
     */
604
    public function doesAuthorHaveAccess($item_limit = null)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$item_limit" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$item_limit"; expected 0 but found 1
Loading history...
605
    {
606
        $can_access = false;
607
608
        if (!isset($item_limit) || $item_limit === 'author') {
609
            $can_access = true;
610
        } elseif ($item_limit === 'developer' && Symphony::Author()->isDeveloper()) {
611
            $can_access = true;
612
        } elseif ($item_limit === 'manager' && (Symphony::Author()->isManager() || Symphony::Author()->isDeveloper())) {
613
            $can_access = true;
614
        } elseif ($item_limit === 'primary' && Symphony::Author()->isPrimaryAccount()) {
615
            $can_access = true;
616
        }
617
618
        return $can_access;
619
    }
620
621
    /**
622
     * Appends the `$this->Header`, `$this->Context` and `$this->Contents`
623
     * to `$this->Wrapper` before adding the ID and class attributes for
624
     * the `<body>` element. This function will also place any Drawer elements
625
     * in their relevant positions in the page. After this has completed the
626
     * parent `generate()` is called which will convert the `XMLElement`'s
627
     * into strings ready for output.
628
     *
629
     * @see core.HTMLPage#generate()
630
     * @param null $page
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $page is correct as it would always require null to be passed?
Loading history...
631
     * @return string
632
     */
633
    public function generate($page = null)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$page" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$page"; expected 0 but found 1
Loading history...
634
    {
635
        $this->Wrapper->appendChild($this->Header);
636
637
        // Add horizontal drawers (inside #context)
638
        if (isset($this->Drawer['horizontal'])) {
639
            $this->Context->appendChildArray($this->Drawer['horizontal']);
640
        }
641
642
        $this->Wrapper->appendChild($this->Context);
643
644
        // Add vertical-left drawers (between #context and #contents)
645
        if (isset($this->Drawer['vertical-left'])) {
646
            $this->Contents->appendChildArray($this->Drawer['vertical-left']);
647
        }
648
649
        // Add vertical-right drawers (after #contents)
650
        if (isset($this->Drawer['vertical-right'])) {
651
            $this->Contents->appendChildArray($this->Drawer['vertical-right']);
652
        }
653
654
        $this->Wrapper->appendChild($this->Contents);
655
656
        $this->Body->appendChild($this->Wrapper);
657
658
        $this->__appendBodyId();
659
        $this->__appendBodyClass($this->_context);
660
661
        /**
662
         * This is just prior to the page headers being rendered, and is suitable for changing them
663
         * @delegate PreRenderHeaders
664
         * @since Symphony 2.7.0
665
         * @param string $context
666
         * '/backend/'
667
         */
668
        Symphony::ExtensionManager()->notifyMembers('PreRenderHeaders', '/backend/');
669
670
        return parent::generate($page);
671
    }
672
673
    /**
674
     * Uses this pages PHP classname as the `<body>` ID attribute.
675
     * This function removes 'content' from the start of the classname
676
     * and converts all uppercase letters to lowercase and prefixes them
677
     * with a hyphen.
678
     */
679
    private function __appendBodyId()
680
    {
681
        // trim "content" from beginning of class name
682
        $body_id = preg_replace("/^content/", '', get_class($this));
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal /^content/ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
683
684
        // lowercase any uppercase letters and prefix with a hyphen
685
        $body_id = trim(
686
            preg_replace_callback(
687
                "/([A-Z])/",
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal /([A-Z])/ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
688
                function($id) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
689
                    return "-" . strtolower($id[0]);
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal - does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
690
                },
691
                $body_id
692
            ),
693
            '-'
694
        );
695
696
        if (!empty($body_id)) {
697
            $this->Body->setAttribute('id', trim($body_id));
698
        }
699
    }
700
701
    /**
702
     * Given the context of the current page, which is an associative
703
     * array, this function will append the values to the page's body as
704
     * classes. If an context value is numeric it will be prepended by 'id-',
705
     * otherwise all classes will be prefixed by the context key.
706
     *
707
     * @param array $context
708
     */
709
    private function __appendBodyClass(array $context = array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$context" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$context"; expected 0 but found 1
Loading history...
710
    {
711
        $body_class = '';
712
713
        foreach ($context as $key => $value) {
714
            if (is_numeric($value)) {
715
                $value = 'id-' . $value;
716
717
                // Add prefixes to all context values by making the
718
                // class be {key}-{value}. #1397 ^BA
719
            } elseif (!is_numeric($key) && isset($value)) {
720
                // Skip arrays
721
                if (is_array($value)) {
722
                    $value = null;
723
                } else {
724
                    $value = str_replace('_', '-', $key) . '-'. $value;
725
                }
726
            }
727
728
            if ($value !== null) {
729
                $body_class .= trim($value) . ' ';
730
            }
731
        }
732
733
        $classes = array_merge(explode(' ', trim($body_class)), explode(' ', trim($this->_body_class)));
734
        $body_class = trim(implode(' ', $classes));
735
736
        if (!empty($body_class)) {
737
            $this->Body->setAttribute('class', $body_class);
738
        }
739
    }
740
741
    /**
742
     * Called to build the content for the page. This function immediately calls
743
     * `__switchboard()` which acts a bit of a controller to show content based on
744
     * off a type, such as 'view' or 'action'. `AdministrationPages` can override this
745
     * function to just display content if they do not need the switchboard functionality
746
     *
747
     * @see __switchboard()
748
     */
749
    public function view()
750
    {
751
        $this->__switchboard();
752
    }
753
754
    /**
755
     * This function is called when `$_REQUEST` contains a key of 'action'.
756
     * Any logic that needs to occur immediately for the action to complete
757
     * should be contained within this function. By default this calls the
758
     * `__switchboard` with the type set to 'action'.
759
     *
760
     * @see __switchboard()
761
     */
762
    public function action()
763
    {
764
        $this->__switchboard('action');
765
    }
766
767
    /**
768
     * The `__switchboard` function acts as a controller to display content
769
     * based off the $type. By default, the `$type` is 'view' but it can be set
770
     * also set to 'action'. The `$type` is prepended by __ and the context is
771
     * append to the $type to create the name of the function that will provide
772
     * that logic. For example, if the $type was action and the context of the
773
     * current page was new, the resulting function to be called would be named
774
     * `__actionNew()`. If an action function is not provided by the Page, this function
775
     * returns nothing, however if a view function is not provided, a 404 page
776
     * will be returned.
777
     *
778
     * @param string $type
779
     *  Either 'view' or 'action', by default this will be 'view'
780
     * @throws SymphonyErrorPage
781
     */
782
    public function __switchboard($type = 'view')
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$type" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$type"; expected 0 but found 1
Loading history...
783
    {
784
        if (!isset($this->_context[0]) || trim($this->_context[0]) === '') {
785
            $context = 'index';
786
        } else {
787
            $context = $this->_context[0];
788
        }
789
790
        $function = ($type == 'action' ? '__action' : '__view') . ucfirst($context);
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
791
792
        if (!method_exists($this, $function)) {
793
            // If there is no action function, just return without doing anything
794
            if ($type == 'action') {
795
                return;
796
            }
797
798
            Administration::instance()->errorPageNotFound();
799
        }
800
801
        $this->$function(null);
802
    }
803
804
    /**
805
     * If `$this->Alert` is set, it will be added to this page. The
806
     * `AppendPageAlert` delegate is fired to allow extensions to provide their
807
     * their own Alert messages for this page. Since Symphony 2.3, there may be
808
     * more than one `Alert` per page. Alerts are displayed in the order of
809
     * severity, with Errors first, then Success alerts followed by Notices.
810
     *
811
     * @uses AppendPageAlert
812
     */
813
    public function appendAlert()
814
    {
815
        /**
816
         * Allows for appending of alerts. Administration::instance()->Page->Alert is way to tell what
817
         * is currently in the system
818
         *
819
         * @delegate AppendPageAlert
820
         * @param string $context
821
         *  '/backend/'
822
         */
823
        Symphony::ExtensionManager()->notifyMembers('AppendPageAlert', '/backend/');
824
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
825
826
        if (!is_array($this->Alert) || empty($this->Alert)) {
0 ignored issues
show
introduced by
The condition is_array($this->Alert) is always true.
Loading history...
827
            return;
828
        }
829
830
        usort($this->Alert, array($this, 'sortAlerts'));
831
832
        // Using prependChild ruins our order (it's backwards, but with most
833
        // recent notices coming after oldest notices), so reversing the array
834
        // fixes this. We need to prepend so that without Javascript the notices
835
        // are at the top of the markup. See #1312
836
        $this->Alert = array_reverse($this->Alert);
837
838
        foreach ($this->Alert as $alert) {
839
            $this->Header->prependChild($alert->asXML());
840
        }
841
    }
842
843
    // Errors first, success next, then notices.
844
    public function sortAlerts($a, $b)
845
    {
846
        if ($a->{'type'} === $b->{'type'}) {
847
            return 0;
848
        }
849
850
        if (
0 ignored issues
show
Coding Style introduced by
Expected 0 spaces after opening bracket; newline found
Loading history...
851
            ($a->{'type'} === Alert::ERROR && $a->{'type'} !== $b->{'type'})
852
            || ($a->{'type'} === Alert::SUCCESS && $b->{'type'} === Alert::NOTICE)
853
        ) {
854
            return -1;
855
        }
856
857
        return 1;
858
    }
859
860
    /**
861
     * This function will append the Navigation to the AdministrationPage.
862
     * It fires a delegate, NavigationPreRender, to allow extensions to manipulate
863
     * the navigation. Extensions should not use this to add their own navigation,
864
     * they should provide the navigation through their fetchNavigation function.
865
     * Note with the Section navigation groups, if there is only one section in a group
866
     * and that section is set to visible, the group will not appear in the navigation.
867
     *
868
     * @uses NavigationPreRender
869
     * @see getNavigationArray()
870
     * @see toolkit.Extension#fetchNavigation()
871
     */
872
    public function appendNavigation()
873
    {
874
        $nav = $this->getNavigationArray();
875
876
        /**
877
         * Immediately before displaying the admin navigation. Provided with the
878
         * navigation array. Manipulating it will alter the navigation for all pages.
879
         *
880
         * @delegate NavigationPreRender
881
         * @param string $context
882
         *  '/backend/'
883
         * @param array $nav
884
         *  An associative array of the current navigation, passed by reference
885
         */
886
        Symphony::ExtensionManager()->notifyMembers('NavigationPreRender', '/backend/', array(
887
            'navigation' => &$nav,
888
        ));
889
890
        $navElement = new XMLElement('nav', null, array('id' => 'nav', 'role' => 'navigation'));
891
        $contentNav = new XMLElement('ul', null, array('class' => 'content', 'role' => 'menubar'));
892
        $structureNav = new XMLElement('ul', null, array('class' => 'structure', 'role' => 'menubar'));
893
894
        foreach ($nav as $n) {
895
            if (isset($n['visible']) && $n['visible'] === 'no') {
896
                continue;
897
            }
898
899
            $item_limit = isset($n['limit']) ? $n['limit'] : null;
900
901
            if ($this->doesAuthorHaveAccess($item_limit)) {
902
                $xGroup = new XMLElement('li', General::sanitize($n['name']), array('role' => 'presentation'));
903
904
                if (isset($n['class']) && trim($n['name']) !== '') {
905
                    $xGroup->setAttribute('class', $n['class']);
906
                }
907
908
                $hasChildren = false;
909
                $xChildren = new XMLElement('ul', null, array('role' => 'menu'));
910
911
                if (is_array($n['children']) && !empty($n['children'])) {
912
                    foreach ($n['children'] as $c) {
913
                        // adapt for Yes and yes
914
                        if (strtolower($c['visible']) !== 'yes') {
915
                            continue;
916
                        }
917
918
                        $child_item_limit = isset($c['limit']) ? $c['limit'] : null;
919
920
                        if ($this->doesAuthorHaveAccess($child_item_limit)) {
921
                            $xChild = new XMLElement('li');
922
                            $xChild->setAttribute('role', 'menuitem');
923
                            $linkChild = Widget::Anchor(General::sanitize($c['name']), SYMPHONY_URL . $c['link']);
0 ignored issues
show
Bug introduced by
The constant SYMPHONY_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
924
                            if (isset($c['target'])) {
925
                                $linkChild->setAttribute('target', $c['target']);
926
                            }
0 ignored issues
show
Coding Style introduced by
No blank line found after control structure
Loading history...
927
                            $xChild->appendChild($linkChild);
928
                            $xChildren->appendChild($xChild);
929
                            $hasChildren = true;
930
                        }
931
                    }
932
933
                    if ($hasChildren) {
934
                        $xGroup->setAttribute('aria-haspopup', 'true');
935
                        $xGroup->appendChild($xChildren);
936
937
                        if ($n['type'] === 'content') {
938
                            $contentNav->appendChild($xGroup);
939
                        } elseif ($n['type'] === 'structure') {
940
                            $structureNav->prependChild($xGroup);
941
                        }
942
                    }
943
                }
944
            }
945
        }
946
947
        $navElement->appendChild($contentNav);
948
        $navElement->appendChild($structureNav);
949
        $this->Header->appendChild($navElement);
950
        Symphony::Profiler()->sample('Navigation Built', PROFILE_LAP);
0 ignored issues
show
Bug introduced by
The constant PROFILE_LAP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
951
    }
952
953
    /**
954
     * Returns the `$_navigation` variable of this Page. If it is empty,
955
     * it will be built by `__buildNavigation`
956
     *
957
     * When it calls `__buildNavigation`, it fires a delegate, NavigationPostBuild,
958
     * to allow extensions to manipulate the navigation.
959
     *
960
     * @uses NavigationPostBuild
961
     * @see __buildNavigation()
962
     * @return array
963
     */
964
    public function getNavigationArray()
965
    {
966
        if (empty($this->_navigation)) {
967
            $this->__buildNavigation();
968
        }
969
970
        return $this->_navigation;
971
    }
972
973
    /**
974
     * This method fills the `$nav` array with value
975
     * from the `ASSETS/xml/navigation.xml` file
976
     *
977
     * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
978
     *
979
     * @since Symphony 2.3.2
980
     *
981
     * @param array $nav
982
     *  The navigation array that will receive nav nodes
983
     */
984
    private function buildXmlNavigation(&$nav)
985
    {
986
        $xml = simplexml_load_file(ASSETS . '/xml/navigation.xml');
0 ignored issues
show
Bug introduced by
The constant ASSETS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
987
988
        // Loop over the default Symphony navigation file, converting
989
        // it into an associative array representation
990
        foreach ($xml->xpath('/navigation/group') as $n) {
991
            $index = (string)$n->attributes()->index;
992
            $children = $n->xpath('children/item');
993
            $content = $n->attributes();
994
995
            // If the index is already set, increment the index and check again.
996
            // Rinse and repeat until the index is not set.
997
            if (isset($nav[$index])) {
998
                do {
999
                    $index++;
1000
                } while (isset($nav[$index]));
1001
            }
1002
1003
            $nav[$index] = array(
1004
                'name' => __(strval($content->name)),
1005
                'type' => 'structure',
1006
                'index' => $index,
1007
                'children' => array()
1008
            );
1009
1010
            if (strlen(trim((string)$content->limit)) > 0) {
1011
                $nav[$index]['limit'] = (string)$content->limit;
1012
            }
1013
1014
            if (count($children) > 0) {
1015
                foreach ($children as $child) {
1016
                    $item = array(
1017
                        'link' => (string)$child->attributes()->link,
1018
                        'name' => __(strval($child->attributes()->name)),
1019
                        'visible' => ((string)$child->attributes()->visible == 'no' ? 'no' : 'yes'),
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
1020
                    );
1021
1022
                    $limit = (string)$child->attributes()->limit;
1023
1024
                    if (strlen(trim($limit)) > 0) {
1025
                        $item['limit'] = $limit;
1026
                    }
1027
1028
                    $nav[$index]['children'][] = $item;
1029
                }
1030
            }
1031
        }
1032
    }
1033
1034
    /**
1035
     * This method fills the `$nav` array with value
1036
     * from each Section
1037
     *
1038
     * @since Symphony 2.3.2
1039
     *
1040
     * @param array $nav
1041
     *  The navigation array that will receive nav nodes
1042
     */
1043
    private function buildSectionNavigation(&$nav)
1044
    {
1045
        // Build the section navigation, grouped by their navigation groups
1046
        $sections = SectionManager::fetch(null, 'asc', 'sortorder');
1047
1048
        if (is_array($sections) && !empty($sections)) {
1049
            foreach ($sections as $s) {
1050
                $group_index = self::__navigationFindGroupIndex($nav, $s->get('navigation_group'));
1051
1052
                if ($group_index === false) {
1053
                    $group_index = General::array_find_available_index($nav, 0);
1054
1055
                    $nav[$group_index] = array(
1056
                        'name' => $s->get('navigation_group'),
1057
                        'type' => 'content',
1058
                        'index' => $group_index,
1059
                        'children' => array()
1060
                    );
1061
                }
1062
1063
                $hasAccess = true;
1064
                $url = '/publish/' . $s->get('handle') . '/';
1065
                /**
1066
                 * Immediately after the core access rules allowed access to this page
1067
                 * (i.e. not called if the core rules denied it).
1068
                 * Extension developers must only further restrict access to it.
1069
                 * Extension developers must also take care of checking the current value
1070
                 * of the allowed parameter in order to prevent conflicts with other extensions.
1071
                 * `$context['allowed'] = $context['allowed'] && customLogic();`
1072
                 *
1073
                 * @delegate CanAccessPage
1074
                 * @since Symphony 2.7.0
1075
                 * @see doesAuthorHaveAccess()
1076
                 * @param string $context
1077
                 *  '/backend/'
1078
                 * @param bool $allowed
1079
                 *  A flag to further restrict access to the page, passed by reference
1080
                 * @param string $page_limit
1081
                 *  The computed page limit for the current page
1082
                 * @param string $page_url
1083
                 *  The computed page url for the current page
1084
                 * @param int $section.id
1085
                 *  The id of the section for this url
1086
                 * @param string $section.handle
1087
                 *  The handle of the section for this url
1088
                 */
1089
                Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array(
1090
                    'allowed' => &$hasAccess,
1091
                    'page_limit' => 'author',
1092
                    'page_url' => $url,
1093
                    'section' => array(
1094
                        'id' => $s->get('id'),
1095
                        'handle' => $s->get('handle')
1096
                    ),
1097
                ));
1098
1099
                if ($hasAccess) {
1100
                    $nav[$group_index]['children'][] = array(
1101
                        'link' => $url,
1102
                        'name' => $s->get('name'),
1103
                        'type' => 'section',
1104
                        'section' => array(
1105
                            'id' => $s->get('id'),
1106
                            'handle' => $s->get('handle')
1107
                        ),
1108
                        'visible' => ($s->get('hidden') == 'no' ? 'yes' : 'no')
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
1109
                    );
1110
                }
1111
            }
1112
        }
1113
    }
1114
1115
    /**
1116
     * This method fills the `$nav` array with value
1117
     * from each Extension's `fetchNavigation` method
1118
     *
1119
     * @since Symphony 2.3.2
1120
     *
1121
     * @param array $nav
1122
     *  The navigation array that will receive nav nodes
1123
     * @throws Exception
1124
     * @throws SymphonyErrorPage
1125
     */
1126
    private function buildExtensionsNavigation(&$nav)
1127
    {
1128
        // Loop over all the installed extensions to add in other navigation items
1129
        $extensions = Symphony::ExtensionManager()->listInstalledHandles();
1130
1131
        foreach ($extensions as $e) {
1132
            $extension = Symphony::ExtensionManager()->getInstance($e);
1133
            $extension_navigation = $extension->fetchNavigation();
1134
1135
            if (is_array($extension_navigation) && !empty($extension_navigation)) {
1136
                foreach ($extension_navigation as $item) {
1137
                    $type = isset($item['children']) ? Extension::NAV_GROUP : Extension::NAV_CHILD;
1138
1139
                    switch ($type) {
1140
                        case Extension::NAV_GROUP:
1141
                            $index = General::array_find_available_index($nav, $item['location']);
1142
1143
                            // Actual group
1144
                            $nav[$index] = self::createParentNavItem($index, $item);
1145
1146
                            // Render its children
1147
                            foreach ($item['children'] as $child) {
1148
                                $nav[$index]['children'][] = self::createChildNavItem($child, $e);
1149
                            }
0 ignored issues
show
Coding Style introduced by
Blank line found after control structure
Loading history...
1150
1151
                            break;
1152
1153
                        case Extension::NAV_CHILD:
1154
                            if (!is_numeric($item['location'])) {
1155
                                // is a navigation group
1156
                                $group_name = $item['location'];
1157
                                $group_index = self::__navigationFindGroupIndex($nav, $item['location']);
1158
                            } else {
1159
                                // is a legacy numeric index
1160
                                $group_index = $item['location'];
1161
                            }
1162
1163
                            $child = self::createChildNavItem($item, $e);
1164
1165
                            if ($group_index === false) {
1166
                                $group_index = General::array_find_available_index($nav, 0);
1167
1168
                                $nav_parent = self::createParentNavItem($group_index, $item);
1169
                                $nav_parent['name'] = $group_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $group_name does not seem to be defined for all execution paths leading up to this point.
Loading history...
1170
                                $nav_parent['children'] = array($child);
1171
1172
                                // add new navigation group
1173
                                $nav[$group_index] = $nav_parent;
1174
                            } else {
1175
                                // add new location by index
1176
                                $nav[$group_index]['children'][] = $child;
1177
                            }
0 ignored issues
show
Coding Style introduced by
Blank line found after control structure
Loading history...
1178
1179
                            break;
1180
                    }
1181
                }
1182
            }
1183
        }
1184
    }
1185
1186
    /**
1187
     * This function builds out a navigation menu item for parents. Parents display
1188
     * in the top level navigation of the backend and may have children (dropdown menus)
1189
     *
1190
     * @since Symphony 2.5.1
1191
     * @param integer $index
1192
     * @param array $item
1193
     * @return array
1194
     */
1195
    private static function createParentNavItem($index, $item)
1196
    {
1197
        $nav_item = array(
1198
            'name' => $item['name'],
1199
            'type' => isset($item['type']) ? $item['type'] : 'structure',
1200
            'index' => $index,
1201
            'children' => array(),
1202
            'limit' => isset($item['limit']) ? $item['limit'] : null
1203
        );
1204
1205
        return $nav_item;
1206
    }
1207
1208
    /**
1209
     * This function builds out a navigation menu item for children. Children
1210
     * live under a parent navigation item and are shown on hover.
1211
     *
1212
     * @since Symphony 2.5.1
1213
     * @param array $item
1214
     * @param string $extension_handle
1215
     * @return array
1216
     */
1217
    private static function createChildNavItem($item, $extension_handle)
1218
    {
1219
        if (!isset($item['relative']) || $item['relative'] === true) {
1220
            $link = '/extension/' . $extension_handle . '/' . ltrim($item['link'], '/');
1221
        } else {
1222
            $link = '/' . ltrim($item['link'], '/');
1223
        }
1224
1225
        $nav_item = array(
1226
            'link' => $link,
1227
            'name' => $item['name'],
1228
            'visible' => (isset($item['visible']) && $item['visible'] == 'no') ? 'no' : 'yes',
1229
            'limit' => isset($item['limit']) ? $item['limit'] : null,
1230
            'target' => isset($item['target']) ? $item['target'] : null
1231
        );
1232
1233
        return $nav_item;
1234
    }
1235
1236
    /**
1237
     * This function populates the `$_navigation` array with an associative array
1238
     * of all the navigation groups and their links. Symphony only supports one
1239
     * level of navigation, so children links cannot have children links. The default
1240
     * Symphony navigation is found in the `ASSETS/xml/navigation.xml` folder. This is
1241
     * loaded first, and then the Section navigation is built, followed by the Extension
1242
     * navigation. Additionally, this function will set the active group of the navigation
1243
     * by checking the current page against the array of links.
1244
     *
1245
     * It fires a delegate, NavigationPostBuild, to allow extensions to manipulate
1246
     * the navigation.
1247
     *
1248
     * @uses NavigationPostBuild
1249
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
1250
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/lib/toolkit/class.extension.php
1251
     */
1252
    public function __buildNavigation()
1253
    {
1254
        $nav = array();
1255
1256
        $this->buildXmlNavigation($nav);
1257
        $this->buildSectionNavigation($nav);
1258
        $this->buildExtensionsNavigation($nav);
1259
1260
        $pageCallback = Administration::instance()->getPageCallback();
1261
1262
        $pageRoot = $pageCallback['pageroot'] . (isset($pageCallback['context'][0]) ? $pageCallback['context'][0] . '/' : '');
1263
        $found = self::__findActiveNavigationGroup($nav, $pageRoot);
1264
1265
        // Normal searches failed. Use a regular expression using the page root. This is less
1266
        // efficient and should never really get invoked unless something weird is going on
1267
        if (!$found) {
1268
            self::__findActiveNavigationGroup($nav, '/^' . str_replace('/', '\/', $pageCallback['pageroot']) . '/i', true);
1269
        }
1270
1271
        ksort($nav);
1272
        $this->_navigation = $nav;
1273
1274
        /**
1275
         * Immediately after the navigation array as been built. Provided with the
1276
         * navigation array. Manipulating it will alter the navigation for all pages.
1277
         * Developers can also alter the 'limit' property of each page to allow more
1278
         * or less access to them.
1279
         * Preventing a user from accessing the page affects both the navigation and the
1280
         * page access rights: user will get a 403 Forbidden error if not authorized.
1281
         *
1282
         * @delegate NavigationPostBuild
1283
         * @since Symphony 2.7.0
1284
         * @param string $context
1285
         *  '/backend/'
1286
         * @param array $nav
1287
         *  An associative array of the current navigation, passed by reference
1288
         */
1289
        Symphony::ExtensionManager()->notifyMembers('NavigationPostBuild', '/backend/', array(
1290
            'navigation' => &$this->_navigation,
1291
        ));
1292
    }
1293
1294
    /**
1295
     * Given an associative array representing the navigation, and a group,
1296
     * this function will attempt to return the index of the group in the navigation
1297
     * array. If it is found, it will return the index, otherwise it will return false.
1298
     *
1299
     * @param array $nav
1300
     *  An associative array of the navigation where the key is the group
1301
     *  index, and the value is an associative array of 'name', 'index' and
1302
     *  'children'. Name is the name of the this group, index is the same as
1303
     *  the key and children is an associative array of navigation items containing
1304
     *  the keys 'link', 'name' and 'visible'. The 'haystack'.
1305
     * @param string $group
1306
     *  The group name to find, the 'needle'.
1307
     * @return integer|boolean
1308
     *  If the group is found, the index will be returned, otherwise false.
1309
     */
1310
    private static function __navigationFindGroupIndex(array $nav, $group)
1311
    {
1312
        foreach ($nav as $index => $item) {
1313
            if ($item['name'] === $group) {
1314
                return $index;
1315
            }
1316
        }
1317
1318
        return false;
1319
    }
1320
1321
    /**
1322
     * Given the navigation array, this function will loop over all the items
1323
     * to determine which is the 'active' navigation group, or in other words,
1324
     * what group best represents the current page `$this->Author` is viewing.
1325
     * This is done by checking the current page's link against all the links
1326
     * provided in the `$nav`, and then flagging the group of the found link
1327
     * with an 'active' CSS class. The current page's link omits any flags or
1328
     * URL parameters and just uses the root page URL.
1329
     *
1330
     * @param array $nav
1331
     *  An associative array of the navigation where the key is the group
1332
     *  index, and the value is an associative array of 'name', 'index' and
1333
     *  'children'. Name is the name of the this group, index is the same as
1334
     *  the key and children is an associative array of navigation items containing
1335
     *  the keys 'link', 'name' and 'visible'. The 'haystack'. This parameter is passed
1336
     *  by reference to this function.
1337
     * @param string $pageroot
1338
     *  The current page the Author is the viewing, minus any flags or URL
1339
     *  parameters such as a Symphony object ID. eg. Section ID, Entry ID. This
1340
     *  parameter is also be a regular expression, but this is highly unlikely.
1341
     * @param boolean $pattern
1342
     *  If set to true, the `$pageroot` represents a regular expression which will
1343
     *  determine if the active navigation item
1344
     * @return boolean
1345
     *  Returns true if an active link was found, false otherwise. If true, the
1346
     *  navigation group of the active link will be given the CSS class 'active'
1347
     */
1348
    private static function __findActiveNavigationGroup(array &$nav, $pageroot, $pattern = false)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$pattern" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$pattern"; expected 0 but found 1
Loading history...
1349
    {
1350
        foreach ($nav as $index => $contents) {
1351
            if (is_array($contents['children']) && !empty($contents['children'])) {
1352
                foreach ($contents['children'] as $item) {
1353
                    if ($pattern && preg_match($pageroot, $item['link'])) {
1354
                        $nav[$index]['class'] = 'active';
1355
                        return true;
1356
                    } elseif ($item['link'] == $pageroot) {
1357
                        $nav[$index]['class'] = 'active';
1358
                        return true;
1359
                    }
1360
                }
1361
            }
1362
        }
1363
1364
        return false;
1365
    }
1366
1367
    /**
1368
     * Creates the Symphony footer for an Administration page. By default
1369
     * this includes the installed Symphony version and the currently logged
1370
     * in Author. A delegate is provided to allow extensions to manipulate the
1371
     * footer HTML, which is an XMLElement of a `<ul>` element.
1372
     * Since Symphony 2.3, it no longer uses the `AddElementToFooter` delegate.
1373
     */
1374
    public function appendUserLinks()
1375
    {
1376
        $ul = new XMLElement('ul', null, array('id' => 'session'));
1377
1378
        $li = new XMLElement('li');
1379
        $li->appendChild(
1380
            Widget::Anchor(
1381
                Symphony::Author()->getFullName(),
1382
                SYMPHONY_URL . '/system/authors/edit/' . Symphony::Author()->get('id') . '/'
0 ignored issues
show
Bug introduced by
The constant SYMPHONY_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1383
            )
1384
        );
1385
        $ul->appendChild($li);
1386
1387
        $li = new XMLElement('li');
1388
        $li->appendChild(Widget::Anchor(__('Log out'), SYMPHONY_URL . '/logout/', null, null, null, array('accesskey' => 'l')));
1389
        $ul->appendChild($li);
1390
1391
        $this->Header->appendChild($ul);
1392
    }
1393
1394
    /**
1395
     * Adds a localized Alert message for failed timestamp validations.
1396
     * It also adds meta information about the last author and timestamp.
1397
     *
1398
     * @since Symphony 2.7.0
1399
     * @param string $errorMessage
1400
     *  The error message to display.
1401
     * @param Entry|Section $existingObject
1402
     *  The Entry or section object that failed validation.
1403
     * @param string $action
1404
     *  The requested action.
1405
     */
1406
    public function addTimestampValidationPageAlert($errorMessage, $existingObject, $action)
1407
    {
1408
        $authorId = $existingObject->get('modification_author_id');
1409
        if (!$authorId) {
1410
            $authorId = $existingObject->get('author_id');
1411
        }
0 ignored issues
show
Coding Style introduced by
No blank line found after control structure
Loading history...
1412
        $author = AuthorManager::fetchByID($authorId);
0 ignored issues
show
Bug introduced by
It seems like $authorId can also be of type string; however, parameter $id of AuthorManager::fetchByID() does only seem to accept array|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

1412
        $author = AuthorManager::fetchByID(/** @scrutinizer ignore-type */ $authorId);
Loading history...
1413
        $formatteAuthorName = $authorId === Symphony::Author()->get('id')
1414
            ? __('yourself')
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement must be declared on a single line
Loading history...
1415
            : (!$author
1416
                ? __('an unknown user')
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement must be declared on a single line
Loading history...
1417
                : $author->get('first_name') . ' ' . $author->get('last_name'));
1418
1419
        $msg = $this->_errors['timestamp'] . ' ' . __(
1420
            'made by %s at %s.', array(
1421
                $formatteAuthorName,
1422
                Widget::Time($existingObject->get('modification_date'))->generate(),
0 ignored issues
show
Bug introduced by
It seems like $existingObject->get('modification_date') can also be of type array; however, parameter $string of Widget::Time() 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

1422
                Widget::Time(/** @scrutinizer ignore-type */ $existingObject->get('modification_date'))->generate(),
Loading history...
1423
            )
1424
        );
1425
1426
        $currentUrl = Administration::instance()->getCurrentPageURL();
1427
        $overwritelink = Widget::Anchor(
1428
            __('Replace changes?'),
1429
            $currentUrl,
1430
            __('Overwrite'),
1431
            'js-tv-overwrite',
1432
            null,
1433
            array(
1434
                'data-action' => General::sanitize($action)
1435
            )
1436
        );
1437
        $ignorelink = Widget::Anchor(
1438
            __('View changes.'),
1439
            $currentUrl,
1440
            __('View the updated entry')
1441
        );
1442
        $actions = $overwritelink->generate() . ' ' . $ignorelink->generate();
1443
        
1444
        $this->pageAlert("$msg $actions", Alert::ERROR);
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $msg instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $actions instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
1445
    }
1446
}
1447