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.
Passed
Pull Request — master (#2837)
by
unknown
05:48
created

AdministrationPage   F

Complexity

Total Complexity 176

Size/Duplication

Total Lines 1424
Duplicated Lines 0 %

Importance

Changes 14
Bugs 0 Features 0
Metric Value
c 14
b 0
f 0
dl 0
loc 1424
rs 0.6314
wmc 176

32 Methods

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

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')
123
    {
124
        $this->setBodyClass($type == 'form' || $type == 'single' ? 'single' : 'index');
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)
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)
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)
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')
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)
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())
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
        $this->addElementToHead(
402
            new XMLElement('script', json_encode($environment), array(
403
                'type' => 'application/json',
404
                'id' => 'environment'
405
            )),
406
            4
407
        );
408
409
        $this->addScriptToHead(ASSETS_URL . '/js/symphony.min.js', 6, false);
410
411
        // Initialise page containers
412
        $this->Wrapper = new XMLElement('div', null, array('id' => 'wrapper'));
413
        $this->Header = new XMLElement('header', null, array('id' => 'header'));
414
        $this->Context = new XMLElement('div', null, array('id' => 'context'));
415
        $this->Breadcrumbs = new XMLElement('div', null, array('id' => 'breadcrumbs'));
416
        $this->Contents = new XMLElement('div', null, array('id' => 'contents', 'role' => 'main'));
417
        $this->Form = Widget::Form(Administration::instance()->getCurrentPageURL(), 'post', null, null, array('role' => 'form'));
418
419
        /**
420
         * Allows developers to insert items into the page HEAD. Use
421
         * `Administration::instance()->Page` for access to the page object.
422
         *
423
         * @since In Symphony 2.3.2 this delegate was renamed from
424
         *  `InitaliseAdminPageHead` to the correct spelling of
425
         *  `InitialiseAdminPageHead`. The old delegate is supported
426
         *  until Symphony 3.0
427
         *
428
         * @delegate InitialiseAdminPageHead
429
         * @param string $context
430
         *  '/backend/'
431
         */
432
        Symphony::ExtensionManager()->notifyMembers('InitialiseAdminPageHead', '/backend/');
433
        Symphony::ExtensionManager()->notifyMembers('InitaliseAdminPageHead', '/backend/');
434
435
        $this->addHeaderToPage('Content-Type', 'text/html; charset=UTF-8');
436
        $this->addHeaderToPage('Cache-Control', 'no-cache, must-revalidate, max-age=0');
437
        $this->addHeaderToPage('Expires', 'Mon, 12 Dec 1982 06:14:00 GMT');
438
        $this->addHeaderToPage('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
439
        $this->addHeaderToPage('Pragma', 'no-cache');
440
441
        // If not set by another extension, lock down the backend
442
        if (!array_key_exists('x-frame-options', $this->headers())) {
443
            $this->addHeaderToPage('X-Frame-Options', 'SAMEORIGIN');
444
        }
445
446
        if (!array_key_exists('x-content-type-options', $this->headers())) {
447
            $this->addHeaderToPage('X-Content-Type-Options', 'nosniff');
448
        }
449
450
        if (!array_key_exists('x-xss-protection', $this->headers())) {
451
            $this->addHeaderToPage('X-XSS-Protection', '1; mode=block');
452
        }
453
454
        if (!array_key_exists('referrer-policy', $this->headers())) {
455
            $this->addHeaderToPage('Referrer-Policy', 'same-origin');
456
        }
457
458
        if (isset($_REQUEST['action'])) {
459
            $this->action();
460
            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...
461
        }
462
463
        $h1 = new XMLElement('h1');
464
        $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

464
        $h1->appendChild(Widget::Anchor(/** @scrutinizer ignore-type */ Symphony::Configuration()->get('sitename', 'general'), rtrim(URL, '/') . '/'));
Loading history...
465
        $this->Header->appendChild($h1);
466
467
        $this->appendUserLinks();
468
        $this->appendNavigation();
469
470
        // Add Breadcrumbs
471
        $this->Context->prependChild($this->Breadcrumbs);
472
        $this->Contents->appendChild($this->Form);
473
474
        // Validate date time config
475
        $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...
476
        if (empty($dateFormat)) {
477
            $this->pageAlert(
478
                __('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...
479
                Alert::NOTICE
480
            );
481
        }
482
        $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...
483
        if (empty($timeFormat)) {
484
            $this->pageAlert(
485
                __('Your <code>%s</code> file does not define a time format.', array(basename(CONFIG))),
486
                Alert::NOTICE
487
            );
488
        }
489
490
        $this->view();
491
492
        $this->appendAlert();
493
494
        Symphony::Profiler()->sample('Page content created', PROFILE_LAP);
495
    }
496
497
    /**
498
     * Checks the current Symphony Author can access the current page.
499
     * This check uses the `ASSETS . /xml/navigation.xml` file to determine
500
     * if the current page (or the current page namespace) can be viewed
501
     * by the currently logged in Author.
502
     *
503
     * @since Symphony 2.7.0
504
     * It fires a delegate, CanAccessPage, to allow extensions to restrict access
505
     * to the current page
506
     *
507
     * @uses CanAccessPage
508
     *
509
     * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
510
     * @return boolean
511
     *  true if the Author can access the current page, false otherwise
512
     */
513
    public function canAccessPage()
514
    {
515
        $nav = $this->getNavigationArray();
516
        $page = '/' . trim(getCurrentPage(), '/') . '/';
517
518
        $page_limit = 'author';
519
520
        foreach ($nav as $item) {
521
            if (
522
                // If page directly matches one of the children
523
                General::in_array_multi($page, $item['children'])
524
                // If the page namespace matches one of the children (this will usually drop query
525
                // string parameters such as /edit/1/)
526
                || General::in_array_multi(Symphony::getPageNamespace() . '/', $item['children'])
527
            ) {
528
                if (is_array($item['children'])) {
529
                    foreach ($item['children'] as $c) {
530
                        if ($c['link'] === $page && isset($c['limit'])) {
531
                            $page_limit = $c['limit'];
532
                            // TODO: break out of the loop here in Symphony 3.0.0
533
                        }
534
                    }
535
                }
536
537
                if (isset($item['limit']) && $page_limit !== 'primary') {
538
                    if ($page_limit === 'author' && $item['limit'] === 'developer') {
539
                        $page_limit = 'developer';
540
                    }
541
                }
542
            } elseif (isset($item['link']) && $page === $item['link'] && isset($item['limit'])) {
543
                $page_limit = $item['limit'];
544
            }
545
        }
546
547
        $hasAccess = $this->doesAuthorHaveAccess($page_limit);
548
549
        if ($hasAccess) {
550
            $page_context = $this->getContext();
551
            $section_handle = !isset($page_context['section_handle']) ? null : $page_context['section_handle'];
552
            /**
553
             * Immediately after the core access rules allowed access to this page
554
             * (i.e. not called if the core rules denied it).
555
             * Extension developers must only further restrict access to it.
556
             * Extension developers must also take care of checking the current value
557
             * of the allowed parameter in order to prevent conflicts with other extensions.
558
             * `$context['allowed'] = $context['allowed'] && customLogic();`
559
             *
560
             * @delegate CanAccessPage
561
             * @since Symphony 2.7.0
562
             * @see doesAuthorHaveAccess()
563
             * @param string $context
564
             *  '/backend/'
565
             * @param bool $allowed
566
             *  A flag to further restrict access to the page, passed by reference
567
             * @param string $page_limit
568
             *  The computed page limit for the current page
569
             * @param string $page_url
570
             *  The computed page url for the current page
571
             * @param int $section.id
572
             *  The id of the section for this url
573
             * @param string $section.handle
574
             *  The handle of the section for this url
575
             */
576
            Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array(
577
                'allowed' => &$hasAccess,
578
                'page_limit' => $page_limit,
579
                'page_url' => $page,
580
                'section' => array(
581
                    'id' => !$section_handle ? 0 : SectionManager::fetchIDFromHandle($section_handle),
582
                    'handle' => $section_handle
583
                ),
584
            ));
585
        }
586
587
        return $hasAccess;
588
    }
589
590
    /**
591
     * Given the limit of the current navigation item or page, this function
592
     * returns if the current Author can access that item or not.
593
     *
594
     * @since Symphony 2.5.1
595
     * @param string $item_limit
596
     * @return boolean
597
     */
598
    public function doesAuthorHaveAccess($item_limit = null)
599
    {
600
        $can_access = false;
601
602
        if (!isset($item_limit) || $item_limit === 'author') {
603
            $can_access = true;
604
        } elseif ($item_limit === 'developer' && Symphony::Author()->isDeveloper()) {
605
            $can_access = true;
606
        } elseif ($item_limit === 'manager' && (Symphony::Author()->isManager() || Symphony::Author()->isDeveloper())) {
607
            $can_access = true;
608
        } elseif ($item_limit === 'primary' && Symphony::Author()->isPrimaryAccount()) {
609
            $can_access = true;
610
        }
611
612
        return $can_access;
613
    }
614
615
    /**
616
     * Appends the `$this->Header`, `$this->Context` and `$this->Contents`
617
     * to `$this->Wrapper` before adding the ID and class attributes for
618
     * the `<body>` element. This function will also place any Drawer elements
619
     * in their relevant positions in the page. After this has completed the
620
     * parent `generate()` is called which will convert the `XMLElement`'s
621
     * into strings ready for output.
622
     *
623
     * @see core.HTMLPage#generate()
624
     * @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...
625
     * @return string
626
     */
627
    public function generate($page = null)
628
    {
629
        $this->Wrapper->appendChild($this->Header);
630
631
        // Add horizontal drawers (inside #context)
632
        if (isset($this->Drawer['horizontal'])) {
633
            $this->Context->appendChildArray($this->Drawer['horizontal']);
634
        }
635
636
        $this->Wrapper->appendChild($this->Context);
637
638
        // Add vertical-left drawers (between #context and #contents)
639
        if (isset($this->Drawer['vertical-left'])) {
640
            $this->Contents->appendChildArray($this->Drawer['vertical-left']);
641
        }
642
643
        // Add vertical-right drawers (after #contents)
644
        if (isset($this->Drawer['vertical-right'])) {
645
            $this->Contents->appendChildArray($this->Drawer['vertical-right']);
646
        }
647
648
        $this->Wrapper->appendChild($this->Contents);
649
650
        $this->Body->appendChild($this->Wrapper);
651
652
        $this->__appendBodyId();
653
        $this->__appendBodyClass($this->_context);
654
655
        /**
656
         * This is just prior to the page headers being rendered, and is suitable for changing them
657
         * @delegate PreRenderHeaders
658
         * @since Symphony 2.7.0
659
         * @param string $context
660
         * '/backend/'
661
         */
662
        Symphony::ExtensionManager()->notifyMembers('PreRenderHeaders', '/backend/');
663
664
        return parent::generate($page);
665
    }
666
667
    /**
668
     * Uses this pages PHP classname as the `<body>` ID attribute.
669
     * This function removes 'content' from the start of the classname
670
     * and converts all uppercase letters to lowercase and prefixes them
671
     * with a hyphen.
672
     */
673
    private function __appendBodyId()
674
    {
675
        // trim "content" from beginning of class name
676
        $body_id = preg_replace("/^content/", '', get_class($this));
677
678
        // lowercase any uppercase letters and prefix with a hyphen
679
        $body_id = trim(
680
            preg_replace_callback(
681
                "/([A-Z])/",
682
                function($id) {
683
                    return "-" . strtolower($id[0]);
684
                },
685
                $body_id
686
            ),
687
            '-'
688
        );
689
690
        if (!empty($body_id)) {
691
            $this->Body->setAttribute('id', trim($body_id));
692
        }
693
    }
694
695
    /**
696
     * Given the context of the current page, which is an associative
697
     * array, this function will append the values to the page's body as
698
     * classes. If an context value is numeric it will be prepended by 'id-',
699
     * otherwise all classes will be prefixed by the context key.
700
     *
701
     * @param array $context
702
     */
703
    private function __appendBodyClass(array $context = array())
704
    {
705
        $body_class = '';
706
707
        foreach ($context as $key => $value) {
708
            if (is_numeric($value)) {
709
                $value = 'id-' . $value;
710
711
                // Add prefixes to all context values by making the
712
                // class be {key}-{value}. #1397 ^BA
713
            } elseif (!is_numeric($key) && isset($value)) {
714
                // Skip arrays
715
                if (is_array($value)) {
716
                    $value = null;
717
                } else {
718
                    $value = str_replace('_', '-', $key) . '-'. $value;
719
                }
720
            }
721
722
            if ($value !== null) {
723
                $body_class .= trim($value) . ' ';
724
            }
725
        }
726
727
        $classes = array_merge(explode(' ', trim($body_class)), explode(' ', trim($this->_body_class)));
728
        $body_class = trim(implode(' ', $classes));
729
730
        if (!empty($body_class)) {
731
            $this->Body->setAttribute('class', $body_class);
732
        }
733
    }
734
735
    /**
736
     * Called to build the content for the page. This function immediately calls
737
     * `__switchboard()` which acts a bit of a controller to show content based on
738
     * off a type, such as 'view' or 'action'. `AdministrationPages` can override this
739
     * function to just display content if they do not need the switchboard functionality
740
     *
741
     * @see __switchboard()
742
     */
743
    public function view()
744
    {
745
        $this->__switchboard();
746
    }
747
748
    /**
749
     * This function is called when `$_REQUEST` contains a key of 'action'.
750
     * Any logic that needs to occur immediately for the action to complete
751
     * should be contained within this function. By default this calls the
752
     * `__switchboard` with the type set to 'action'.
753
     *
754
     * @see __switchboard()
755
     */
756
    public function action()
757
    {
758
        $this->__switchboard('action');
759
    }
760
761
    /**
762
     * The `__switchboard` function acts as a controller to display content
763
     * based off the $type. By default, the `$type` is 'view' but it can be set
764
     * also set to 'action'. The `$type` is prepended by __ and the context is
765
     * append to the $type to create the name of the function that will provide
766
     * that logic. For example, if the $type was action and the context of the
767
     * current page was new, the resulting function to be called would be named
768
     * `__actionNew()`. If an action function is not provided by the Page, this function
769
     * returns nothing, however if a view function is not provided, a 404 page
770
     * will be returned.
771
     *
772
     * @param string $type
773
     *  Either 'view' or 'action', by default this will be 'view'
774
     * @throws SymphonyErrorPage
775
     */
776
    public function __switchboard($type = 'view')
777
    {
778
        if (!isset($this->_context[0]) || trim($this->_context[0]) === '') {
779
            $context = 'index';
780
        } else {
781
            $context = $this->_context[0];
782
        }
783
784
        $function = ($type == 'action' ? '__action' : '__view') . ucfirst($context);
785
786
        if (!method_exists($this, $function)) {
787
            // If there is no action function, just return without doing anything
788
            if ($type == 'action') {
789
                return;
790
            }
791
792
            Administration::instance()->errorPageNotFound();
793
        }
794
795
        $this->$function(null);
796
    }
797
798
    /**
799
     * If `$this->Alert` is set, it will be added to this page. The
800
     * `AppendPageAlert` delegate is fired to allow extensions to provide their
801
     * their own Alert messages for this page. Since Symphony 2.3, there may be
802
     * more than one `Alert` per page. Alerts are displayed in the order of
803
     * severity, with Errors first, then Success alerts followed by Notices.
804
     *
805
     * @uses AppendPageAlert
806
     */
807
    public function appendAlert()
808
    {
809
        /**
810
         * Allows for appending of alerts. Administration::instance()->Page->Alert is way to tell what
811
         * is currently in the system
812
         *
813
         * @delegate AppendPageAlert
814
         * @param string $context
815
         *  '/backend/'
816
         */
817
        Symphony::ExtensionManager()->notifyMembers('AppendPageAlert', '/backend/');
818
819
820
        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...
821
            return;
822
        }
823
824
        usort($this->Alert, array($this, 'sortAlerts'));
825
826
        // Using prependChild ruins our order (it's backwards, but with most
827
        // recent notices coming after oldest notices), so reversing the array
828
        // fixes this. We need to prepend so that without Javascript the notices
829
        // are at the top of the markup. See #1312
830
        $this->Alert = array_reverse($this->Alert);
831
832
        foreach ($this->Alert as $alert) {
833
            $this->Header->prependChild($alert->asXML());
834
        }
835
    }
836
837
    // Errors first, success next, then notices.
838
    public function sortAlerts($a, $b)
839
    {
840
        if ($a->{'type'} === $b->{'type'}) {
841
            return 0;
842
        }
843
844
        if (
845
            ($a->{'type'} === Alert::ERROR && $a->{'type'} !== $b->{'type'})
846
            || ($a->{'type'} === Alert::SUCCESS && $b->{'type'} === Alert::NOTICE)
847
        ) {
848
            return -1;
849
        }
850
851
        return 1;
852
    }
853
854
    /**
855
     * This function will append the Navigation to the AdministrationPage.
856
     * It fires a delegate, NavigationPreRender, to allow extensions to manipulate
857
     * the navigation. Extensions should not use this to add their own navigation,
858
     * they should provide the navigation through their fetchNavigation function.
859
     * Note with the Section navigation groups, if there is only one section in a group
860
     * and that section is set to visible, the group will not appear in the navigation.
861
     *
862
     * @uses NavigationPreRender
863
     * @see getNavigationArray()
864
     * @see toolkit.Extension#fetchNavigation()
865
     */
866
    public function appendNavigation()
867
    {
868
        $nav = $this->getNavigationArray();
869
870
        /**
871
         * Immediately before displaying the admin navigation. Provided with the
872
         * navigation array. Manipulating it will alter the navigation for all pages.
873
         *
874
         * @delegate NavigationPreRender
875
         * @param string $context
876
         *  '/backend/'
877
         * @param array $nav
878
         *  An associative array of the current navigation, passed by reference
879
         */
880
        Symphony::ExtensionManager()->notifyMembers('NavigationPreRender', '/backend/', array(
881
            'navigation' => &$nav,
882
        ));
883
884
        $navElement = new XMLElement('nav', null, array('id' => 'nav', 'role' => 'navigation'));
885
        $contentNav = new XMLElement('ul', null, array('class' => 'content', 'role' => 'menubar'));
886
        $structureNav = new XMLElement('ul', null, array('class' => 'structure', 'role' => 'menubar'));
887
888
        foreach ($nav as $n) {
889
            if (isset($n['visible']) && $n['visible'] === 'no') {
890
                continue;
891
            }
892
893
            $item_limit = isset($n['limit']) ? $n['limit'] : null;
894
895
            if ($this->doesAuthorHaveAccess($item_limit)) {
896
                $xGroup = new XMLElement('li', General::sanitize($n['name']), array('role' => 'presentation'));
897
898
                if (isset($n['class']) && trim($n['name']) !== '') {
899
                    $xGroup->setAttribute('class', $n['class']);
900
                }
901
902
                $hasChildren = false;
903
                $xChildren = new XMLElement('ul', null, array('role' => 'menu'));
904
905
                if (is_array($n['children']) && !empty($n['children'])) {
906
                    foreach ($n['children'] as $c) {
907
                        // adapt for Yes and yes
908
                        if (strtolower($c['visible']) !== 'yes') {
909
                            continue;
910
                        }
911
912
                        $child_item_limit = isset($c['limit']) ? $c['limit'] : null;
913
914
                        if ($this->doesAuthorHaveAccess($child_item_limit)) {
915
                            $xChild = new XMLElement('li');
916
                            $xChild->setAttribute('role', 'menuitem');
917
                            $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...
918
                            if (isset($c['target'])) {
919
                                $linkChild->setAttribute('target', $c['target']);
920
                            }
921
                            $xChild->appendChild($linkChild);
922
                            $xChildren->appendChild($xChild);
923
                            $hasChildren = true;
924
                        }
925
                    }
926
927
                    if ($hasChildren) {
928
                        $xGroup->setAttribute('aria-haspopup', 'true');
929
                        $xGroup->appendChild($xChildren);
930
931
                        if ($n['type'] === 'content') {
932
                            $contentNav->appendChild($xGroup);
933
                        } elseif ($n['type'] === 'structure') {
934
                            $structureNav->prependChild($xGroup);
935
                        }
936
                    }
937
                }
938
            }
939
        }
940
941
        $navElement->appendChild($contentNav);
942
        $navElement->appendChild($structureNav);
943
        $this->Header->appendChild($navElement);
944
        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...
945
    }
946
947
    /**
948
     * Returns the `$_navigation` variable of this Page. If it is empty,
949
     * it will be built by `__buildNavigation`
950
     *
951
     * When it calls `__buildNavigation`, it fires a delegate, NavigationPostBuild,
952
     * to allow extensions to manipulate the navigation.
953
     *
954
     * @uses NavigationPostBuild
955
     * @see __buildNavigation()
956
     * @return array
957
     */
958
    public function getNavigationArray()
959
    {
960
        if (empty($this->_navigation)) {
961
            $this->__buildNavigation();
962
        }
963
964
        return $this->_navigation;
965
    }
966
967
    /**
968
     * This method fills the `$nav` array with value
969
     * from the `ASSETS/xml/navigation.xml` file
970
     *
971
     * @link http://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
972
     *
973
     * @since Symphony 2.3.2
974
     *
975
     * @param array $nav
976
     *  The navigation array that will receive nav nodes
977
     */
978
    private function buildXmlNavigation(&$nav)
979
    {
980
        $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...
981
982
        // Loop over the default Symphony navigation file, converting
983
        // it into an associative array representation
984
        foreach ($xml->xpath('/navigation/group') as $n) {
985
            $index = (string)$n->attributes()->index;
986
            $children = $n->xpath('children/item');
987
            $content = $n->attributes();
988
989
            // If the index is already set, increment the index and check again.
990
            // Rinse and repeat until the index is not set.
991
            if (isset($nav[$index])) {
992
                do {
993
                    $index++;
994
                } while (isset($nav[$index]));
995
            }
996
997
            $nav[$index] = array(
998
                'name' => __(strval($content->name)),
999
                'type' => 'structure',
1000
                'index' => $index,
1001
                'children' => array()
1002
            );
1003
1004
            if (strlen(trim((string)$content->limit)) > 0) {
1005
                $nav[$index]['limit'] = (string)$content->limit;
1006
            }
1007
1008
            if (count($children) > 0) {
1009
                foreach ($children as $child) {
1010
                    $item = array(
1011
                        'link' => (string)$child->attributes()->link,
1012
                        'name' => __(strval($child->attributes()->name)),
1013
                        'visible' => ((string)$child->attributes()->visible == 'no' ? 'no' : 'yes'),
1014
                    );
1015
1016
                    $limit = (string)$child->attributes()->limit;
1017
1018
                    if (strlen(trim($limit)) > 0) {
1019
                        $item['limit'] = $limit;
1020
                    }
1021
1022
                    $nav[$index]['children'][] = $item;
1023
                }
1024
            }
1025
        }
1026
    }
1027
1028
    /**
1029
     * This method fills the `$nav` array with value
1030
     * from each Section
1031
     *
1032
     * @since Symphony 2.3.2
1033
     *
1034
     * @param array $nav
1035
     *  The navigation array that will receive nav nodes
1036
     */
1037
    private function buildSectionNavigation(&$nav)
1038
    {
1039
        // Build the section navigation, grouped by their navigation groups
1040
        $sections = SectionManager::fetch(null, 'asc', 'sortorder');
1041
1042
        if (is_array($sections) && !empty($sections)) {
1043
            foreach ($sections as $s) {
1044
                $group_index = self::__navigationFindGroupIndex($nav, $s->get('navigation_group'));
1045
1046
                if ($group_index === false) {
1047
                    $group_index = General::array_find_available_index($nav, 0);
1048
1049
                    $nav[$group_index] = array(
1050
                        'name' => $s->get('navigation_group'),
1051
                        'type' => 'content',
1052
                        'index' => $group_index,
1053
                        'children' => array()
1054
                    );
1055
                }
1056
1057
                $hasAccess = true;
1058
                $url = '/publish/' . $s->get('handle') . '/';
1059
                /**
1060
                 * Immediately after the core access rules allowed access to this page
1061
                 * (i.e. not called if the core rules denied it).
1062
                 * Extension developers must only further restrict access to it.
1063
                 * Extension developers must also take care of checking the current value
1064
                 * of the allowed parameter in order to prevent conflicts with other extensions.
1065
                 * `$context['allowed'] = $context['allowed'] && customLogic();`
1066
                 *
1067
                 * @delegate CanAccessPage
1068
                 * @since Symphony 2.7.0
1069
                 * @see doesAuthorHaveAccess()
1070
                 * @param string $context
1071
                 *  '/backend/'
1072
                 * @param bool $allowed
1073
                 *  A flag to further restrict access to the page, passed by reference
1074
                 * @param string $page_limit
1075
                 *  The computed page limit for the current page
1076
                 * @param string $page_url
1077
                 *  The computed page url for the current page
1078
                 * @param int $section.id
1079
                 *  The id of the section for this url
1080
                 * @param string $section.handle
1081
                 *  The handle of the section for this url
1082
                 */
1083
                Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array(
1084
                    'allowed' => &$hasAccess,
1085
                    'page_limit' => 'author',
1086
                    'page_url' => $url,
1087
                    'section' => array(
1088
                        'id' => $s->get('id'),
1089
                        'handle' => $s->get('handle')
1090
                    ),
1091
                ));
1092
1093
                if ($hasAccess) {
1094
                    $nav[$group_index]['children'][] = array(
1095
                        'link' => $url,
1096
                        'name' => $s->get('name'),
1097
                        'type' => 'section',
1098
                        'section' => array(
1099
                            'id' => $s->get('id'),
1100
                            'handle' => $s->get('handle')
1101
                        ),
1102
                        'visible' => ($s->get('hidden') == 'no' ? 'yes' : 'no')
1103
                    );
1104
                }
1105
            }
1106
        }
1107
    }
1108
1109
    /**
1110
     * This method fills the `$nav` array with value
1111
     * from each Extension's `fetchNavigation` method
1112
     *
1113
     * @since Symphony 2.3.2
1114
     *
1115
     * @param array $nav
1116
     *  The navigation array that will receive nav nodes
1117
     * @throws Exception
1118
     * @throws SymphonyErrorPage
1119
     */
1120
    private function buildExtensionsNavigation(&$nav)
1121
    {
1122
        // Loop over all the installed extensions to add in other navigation items
1123
        $extensions = Symphony::ExtensionManager()->listInstalledHandles();
1124
1125
        foreach ($extensions as $e) {
1126
            $extension = Symphony::ExtensionManager()->getInstance($e);
1127
            $extension_navigation = $extension->fetchNavigation();
1128
1129
            if (is_array($extension_navigation) && !empty($extension_navigation)) {
1130
                foreach ($extension_navigation as $item) {
1131
                    $type = isset($item['children']) ? Extension::NAV_GROUP : Extension::NAV_CHILD;
1132
1133
                    switch ($type) {
1134
                        case Extension::NAV_GROUP:
1135
                            $index = General::array_find_available_index($nav, $item['location']);
1136
1137
                            // Actual group
1138
                            $nav[$index] = self::createParentNavItem($index, $item);
1139
1140
                            // Render its children
1141
                            foreach ($item['children'] as $child) {
1142
                                $nav[$index]['children'][] = self::createChildNavItem($child, $e);
1143
                            }
1144
1145
                            break;
1146
1147
                        case Extension::NAV_CHILD:
1148
                            if (!is_numeric($item['location'])) {
1149
                                // is a navigation group
1150
                                $group_name = $item['location'];
1151
                                $group_index = self::__navigationFindGroupIndex($nav, $item['location']);
1152
                            } else {
1153
                                // is a legacy numeric index
1154
                                $group_index = $item['location'];
1155
                            }
1156
1157
                            $child = self::createChildNavItem($item, $e);
1158
1159
                            if ($group_index === false) {
1160
                                $group_index = General::array_find_available_index($nav, 0);
1161
1162
                                $nav_parent = self::createParentNavItem($group_index, $item);
1163
                                $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...
1164
                                $nav_parent['children'] = array($child);
1165
1166
                                // add new navigation group
1167
                                $nav[$group_index] = $nav_parent;
1168
                            } else {
1169
                                // add new location by index
1170
                                $nav[$group_index]['children'][] = $child;
1171
                            }
1172
1173
                            break;
1174
                    }
1175
                }
1176
            }
1177
        }
1178
    }
1179
1180
    /**
1181
     * This function builds out a navigation menu item for parents. Parents display
1182
     * in the top level navigation of the backend and may have children (dropdown menus)
1183
     *
1184
     * @since Symphony 2.5.1
1185
     * @param integer $index
1186
     * @param array $item
1187
     * @return array
1188
     */
1189
    private static function createParentNavItem($index, $item)
1190
    {
1191
        $nav_item = array(
1192
            'name' => $item['name'],
1193
            'type' => isset($item['type']) ? $item['type'] : 'structure',
1194
            'index' => $index,
1195
            'children' => array(),
1196
            'limit' => isset($item['limit']) ? $item['limit'] : null
1197
        );
1198
1199
        return $nav_item;
1200
    }
1201
1202
    /**
1203
     * This function builds out a navigation menu item for children. Children
1204
     * live under a parent navigation item and are shown on hover.
1205
     *
1206
     * @since Symphony 2.5.1
1207
     * @param array $item
1208
     * @param string $extension_handle
1209
     * @return array
1210
     */
1211
    private static function createChildNavItem($item, $extension_handle)
1212
    {
1213
        if (!isset($item['relative']) || $item['relative'] === true) {
1214
            $link = '/extension/' . $extension_handle . '/' . ltrim($item['link'], '/');
1215
        } else {
1216
            $link = '/' . ltrim($item['link'], '/');
1217
        }
1218
1219
        $nav_item = array(
1220
            'link' => $link,
1221
            'name' => $item['name'],
1222
            'visible' => (isset($item['visible']) && $item['visible'] == 'no') ? 'no' : 'yes',
1223
            'limit' => isset($item['limit']) ? $item['limit'] : null,
1224
            'target' => isset($item['target']) ? $item['target'] : null
1225
        );
1226
1227
        return $nav_item;
1228
    }
1229
1230
    /**
1231
     * This function populates the `$_navigation` array with an associative array
1232
     * of all the navigation groups and their links. Symphony only supports one
1233
     * level of navigation, so children links cannot have children links. The default
1234
     * Symphony navigation is found in the `ASSETS/xml/navigation.xml` folder. This is
1235
     * loaded first, and then the Section navigation is built, followed by the Extension
1236
     * navigation. Additionally, this function will set the active group of the navigation
1237
     * by checking the current page against the array of links.
1238
     *
1239
     * It fires a delegate, NavigationPostBuild, to allow extensions to manipulate
1240
     * the navigation.
1241
     *
1242
     * @uses NavigationPostBuild
1243
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
1244
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/lib/toolkit/class.extension.php
1245
     */
1246
    public function __buildNavigation()
1247
    {
1248
        $nav = array();
1249
1250
        $this->buildXmlNavigation($nav);
1251
        $this->buildSectionNavigation($nav);
1252
        $this->buildExtensionsNavigation($nav);
1253
1254
        $pageCallback = Administration::instance()->getPageCallback();
1255
1256
        $pageRoot = $pageCallback['pageroot'] . (isset($pageCallback['context'][0]) ? $pageCallback['context'][0] . '/' : '');
1257
        $found = self::__findActiveNavigationGroup($nav, $pageRoot);
1258
1259
        // Normal searches failed. Use a regular expression using the page root. This is less
1260
        // efficient and should never really get invoked unless something weird is going on
1261
        if (!$found) {
1262
            self::__findActiveNavigationGroup($nav, '/^' . str_replace('/', '\/', $pageCallback['pageroot']) . '/i', true);
1263
        }
1264
1265
        ksort($nav);
1266
        $this->_navigation = $nav;
1267
1268
        /**
1269
         * Immediately after the navigation array as been built. Provided with the
1270
         * navigation array. Manipulating it will alter the navigation for all pages.
1271
         * Developers can also alter the 'limit' property of each page to allow more
1272
         * or less access to them.
1273
         * Preventing a user from accessing the page affects both the navigation and the
1274
         * page access rights: user will get a 403 Forbidden error if not authorized.
1275
         *
1276
         * @delegate NavigationPostBuild
1277
         * @since Symphony 2.7.0
1278
         * @param string $context
1279
         *  '/backend/'
1280
         * @param array $nav
1281
         *  An associative array of the current navigation, passed by reference
1282
         */
1283
        Symphony::ExtensionManager()->notifyMembers('NavigationPostBuild', '/backend/', array(
1284
            'navigation' => &$this->_navigation,
1285
        ));
1286
    }
1287
1288
    /**
1289
     * Given an associative array representing the navigation, and a group,
1290
     * this function will attempt to return the index of the group in the navigation
1291
     * array. If it is found, it will return the index, otherwise it will return false.
1292
     *
1293
     * @param array $nav
1294
     *  An associative array of the navigation where the key is the group
1295
     *  index, and the value is an associative array of 'name', 'index' and
1296
     *  'children'. Name is the name of the this group, index is the same as
1297
     *  the key and children is an associative array of navigation items containing
1298
     *  the keys 'link', 'name' and 'visible'. The 'haystack'.
1299
     * @param string $group
1300
     *  The group name to find, the 'needle'.
1301
     * @return integer|boolean
1302
     *  If the group is found, the index will be returned, otherwise false.
1303
     */
1304
    private static function __navigationFindGroupIndex(array $nav, $group)
1305
    {
1306
        foreach ($nav as $index => $item) {
1307
            if ($item['name'] === $group) {
1308
                return $index;
1309
            }
1310
        }
1311
1312
        return false;
1313
    }
1314
1315
    /**
1316
     * Given the navigation array, this function will loop over all the items
1317
     * to determine which is the 'active' navigation group, or in other words,
1318
     * what group best represents the current page `$this->Author` is viewing.
1319
     * This is done by checking the current page's link against all the links
1320
     * provided in the `$nav`, and then flagging the group of the found link
1321
     * with an 'active' CSS class. The current page's link omits any flags or
1322
     * URL parameters and just uses the root page URL.
1323
     *
1324
     * @param array $nav
1325
     *  An associative array of the navigation where the key is the group
1326
     *  index, and the value is an associative array of 'name', 'index' and
1327
     *  'children'. Name is the name of the this group, index is the same as
1328
     *  the key and children is an associative array of navigation items containing
1329
     *  the keys 'link', 'name' and 'visible'. The 'haystack'. This parameter is passed
1330
     *  by reference to this function.
1331
     * @param string $pageroot
1332
     *  The current page the Author is the viewing, minus any flags or URL
1333
     *  parameters such as a Symphony object ID. eg. Section ID, Entry ID. This
1334
     *  parameter is also be a regular expression, but this is highly unlikely.
1335
     * @param boolean $pattern
1336
     *  If set to true, the `$pageroot` represents a regular expression which will
1337
     *  determine if the active navigation item
1338
     * @return boolean
1339
     *  Returns true if an active link was found, false otherwise. If true, the
1340
     *  navigation group of the active link will be given the CSS class 'active'
1341
     */
1342
    private static function __findActiveNavigationGroup(array &$nav, $pageroot, $pattern = false)
1343
    {
1344
        foreach ($nav as $index => $contents) {
1345
            if (is_array($contents['children']) && !empty($contents['children'])) {
1346
                foreach ($contents['children'] as $item) {
1347
                    if ($pattern && preg_match($pageroot, $item['link'])) {
1348
                        $nav[$index]['class'] = 'active';
1349
                        return true;
1350
                    } elseif ($item['link'] == $pageroot) {
1351
                        $nav[$index]['class'] = 'active';
1352
                        return true;
1353
                    }
1354
                }
1355
            }
1356
        }
1357
1358
        return false;
1359
    }
1360
1361
    /**
1362
     * Creates the Symphony footer for an Administration page. By default
1363
     * this includes the installed Symphony version and the currently logged
1364
     * in Author. A delegate is provided to allow extensions to manipulate the
1365
     * footer HTML, which is an XMLElement of a `<ul>` element.
1366
     * Since Symphony 2.3, it no longer uses the `AddElementToFooter` delegate.
1367
     */
1368
    public function appendUserLinks()
1369
    {
1370
        $ul = new XMLElement('ul', null, array('id' => 'session'));
1371
1372
        $li = new XMLElement('li');
1373
        $li->appendChild(
1374
            Widget::Anchor(
1375
                Symphony::Author()->getFullName(),
1376
                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...
1377
            )
1378
        );
1379
        $ul->appendChild($li);
1380
1381
        $li = new XMLElement('li');
1382
        $li->appendChild(Widget::Anchor(__('Log out'), SYMPHONY_URL . '/logout/', null, null, null, array('accesskey' => 'l')));
1383
        $ul->appendChild($li);
1384
1385
        $this->Header->appendChild($ul);
1386
    }
1387
1388
    /**
1389
     * Adds a localized Alert message for failed timestamp validations.
1390
     * It also adds meta information about the last author and timestamp.
1391
     *
1392
     * @since Symphony 2.7.0
1393
     * @param string $errorMessage
1394
     *  The error message to display.
1395
     * @param Entry|Section $existingObject
1396
     *  The Entry or section object that failed validation.
1397
     * @param string $action
1398
     *  The requested action.
1399
     */
1400
    public function addTimestampValidationPageAlert($errorMessage, $existingObject, $action)
1401
    {
1402
        $authorId = $existingObject->get('modification_author_id');
1403
        if (!$authorId) {
1404
            $authorId = $existingObject->get('author_id');
1405
        }
1406
        $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 integer|array, 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

1406
        $author = AuthorManager::fetchByID(/** @scrutinizer ignore-type */ $authorId);
Loading history...
1407
        $formatteAuthorName = $authorId === Symphony::Author()->get('id')
1408
            ? __('yourself')
1409
            : (!$author
1410
                ? __('an unknown user')
1411
                : $author->get('first_name') . ' ' . $author->get('last_name'));
1412
1413
        $msg = $this->_errors['timestamp'] . ' ' . __(
1414
            'made by %s at %s.', array(
1415
                $formatteAuthorName,
1416
                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

1416
                Widget::Time(/** @scrutinizer ignore-type */ $existingObject->get('modification_date'))->generate(),
Loading history...
1417
            )
1418
        );
1419
1420
        $currentUrl = Administration::instance()->getCurrentPageURL();
1421
        $overwritelink = Widget::Anchor(
1422
            __('Replace changes?'),
1423
            $currentUrl,
1424
            __('Overwrite'),
1425
            'js-tv-overwrite',
1426
            null,
1427
            array(
1428
                'data-action' => General::sanitize($action)
1429
            )
1430
        );
1431
        $ignorelink = Widget::Anchor(
1432
            __('View changes.'),
1433
            $currentUrl,
1434
            __('View the updated entry')
1435
        );
1436
        $actions = $overwritelink->generate() . ' ' . $ignorelink->generate();
1437
        
1438
        $this->pageAlert("$msg $actions", Alert::ERROR);
1439
    }
1440
}
1441