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 (#2835)
by
unknown
06:01
created

addTimestampValidationPageAlert()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 39
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 28
c 0
b 0
f 0
nc 8
nop 3
dl 0
loc 39
rs 8.5806
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>');
0 ignored issues
show
introduced by
The method setDTD() does not exist on XMLElement. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

359
        $this->Html->/** @scrutinizer ignore-call */ 
360
                     setDTD('<!DOCTYPE html>');
Loading history...
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 = (new SectionManager)->select()->sort('sortorder')->execute()->rows();
1041
1042
        foreach ($sections as $s) {
1043
            $group_index = self::__navigationFindGroupIndex($nav, $s->get('navigation_group'));
1044
1045
            if ($group_index === false) {
1046
                $group_index = General::array_find_available_index($nav, 0);
1047
1048
                $nav[$group_index] = array(
1049
                    'name' => $s->get('navigation_group'),
1050
                    'type' => 'content',
1051
                    'index' => $group_index,
1052
                    'children' => array()
1053
                );
1054
            }
1055
1056
            $hasAccess = true;
1057
            $url = '/publish/' . $s->get('handle') . '/';
1058
            /**
1059
             * Immediately after the core access rules allowed access to this page
1060
             * (i.e. not called if the core rules denied it).
1061
             * Extension developers must only further restrict access to it.
1062
             * Extension developers must also take care of checking the current value
1063
             * of the allowed parameter in order to prevent conflicts with other extensions.
1064
             * `$context['allowed'] = $context['allowed'] && customLogic();`
1065
             *
1066
             * @delegate CanAccessPage
1067
             * @since Symphony 2.7.0
1068
             * @see doesAuthorHaveAccess()
1069
             * @param string $context
1070
             *  '/backend/'
1071
             * @param bool $allowed
1072
             *  A flag to further restrict access to the page, passed by reference
1073
             * @param string $page_limit
1074
             *  The computed page limit for the current page
1075
             * @param string $page_url
1076
             *  The computed page url for the current page
1077
             * @param int $section.id
1078
             *  The id of the section for this url
1079
             * @param string $section.handle
1080
             *  The handle of the section for this url
1081
             */
1082
            Symphony::ExtensionManager()->notifyMembers('CanAccessPage', '/backend/', array(
1083
                'allowed' => &$hasAccess,
1084
                'page_limit' => 'author',
1085
                'page_url' => $url,
1086
                'section' => array(
1087
                    'id' => $s->get('id'),
1088
                    'handle' => $s->get('handle')
1089
                ),
1090
            ));
1091
1092
            if ($hasAccess) {
1093
                $nav[$group_index]['children'][] = array(
1094
                    'link' => $url,
1095
                    'name' => $s->get('name'),
1096
                    'type' => 'section',
1097
                    'section' => array(
1098
                        'id' => $s->get('id'),
1099
                        'handle' => $s->get('handle')
1100
                    ),
1101
                    'visible' => ($s->get('hidden') == 'no' ? 'yes' : 'no')
1102
                );
1103
            }
1104
        }
1105
    }
1106
1107
    /**
1108
     * This method fills the `$nav` array with value
1109
     * from each Extension's `fetchNavigation` method
1110
     *
1111
     * @since Symphony 2.3.2
1112
     *
1113
     * @param array $nav
1114
     *  The navigation array that will receive nav nodes
1115
     * @throws Exception
1116
     * @throws SymphonyErrorPage
1117
     */
1118
    private function buildExtensionsNavigation(&$nav)
1119
    {
1120
        // Loop over all the installed extensions to add in other navigation items
1121
        $extensions = Symphony::ExtensionManager()->listInstalledHandles();
1122
1123
        foreach ($extensions as $e) {
1124
            $extension = Symphony::ExtensionManager()->getInstance($e);
1125
            $extension_navigation = $extension->fetchNavigation();
1126
1127
            if (is_array($extension_navigation) && !empty($extension_navigation)) {
1128
                foreach ($extension_navigation as $item) {
1129
                    $type = isset($item['children']) ? Extension::NAV_GROUP : Extension::NAV_CHILD;
1130
1131
                    switch ($type) {
1132
                        case Extension::NAV_GROUP:
1133
                            $index = General::array_find_available_index($nav, $item['location']);
1134
1135
                            // Actual group
1136
                            $nav[$index] = self::createParentNavItem($index, $item);
1137
1138
                            // Render its children
1139
                            foreach ($item['children'] as $child) {
1140
                                $nav[$index]['children'][] = self::createChildNavItem($child, $e);
1141
                            }
1142
1143
                            break;
1144
1145
                        case Extension::NAV_CHILD:
1146
                            if (!is_numeric($item['location'])) {
1147
                                // is a navigation group
1148
                                $group_name = $item['location'];
1149
                                $group_index = self::__navigationFindGroupIndex($nav, $item['location']);
1150
                            } else {
1151
                                // is a legacy numeric index
1152
                                $group_index = $item['location'];
1153
                            }
1154
1155
                            $child = self::createChildNavItem($item, $e);
1156
1157
                            if ($group_index === false) {
1158
                                $group_index = General::array_find_available_index($nav, 0);
1159
1160
                                $nav_parent = self::createParentNavItem($group_index, $item);
1161
                                $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...
1162
                                $nav_parent['children'] = array($child);
1163
1164
                                // add new navigation group
1165
                                $nav[$group_index] = $nav_parent;
1166
                            } else {
1167
                                // add new location by index
1168
                                $nav[$group_index]['children'][] = $child;
1169
                            }
1170
1171
                            break;
1172
                    }
1173
                }
1174
            }
1175
        }
1176
    }
1177
1178
    /**
1179
     * This function builds out a navigation menu item for parents. Parents display
1180
     * in the top level navigation of the backend and may have children (dropdown menus)
1181
     *
1182
     * @since Symphony 2.5.1
1183
     * @param integer $index
1184
     * @param array $item
1185
     * @return array
1186
     */
1187
    private static function createParentNavItem($index, $item)
1188
    {
1189
        $nav_item = array(
1190
            'name' => $item['name'],
1191
            'type' => isset($item['type']) ? $item['type'] : 'structure',
1192
            'index' => $index,
1193
            'children' => array(),
1194
            'limit' => isset($item['limit']) ? $item['limit'] : null
1195
        );
1196
1197
        return $nav_item;
1198
    }
1199
1200
    /**
1201
     * This function builds out a navigation menu item for children. Children
1202
     * live under a parent navigation item and are shown on hover.
1203
     *
1204
     * @since Symphony 2.5.1
1205
     * @param array $item
1206
     * @param string $extension_handle
1207
     * @return array
1208
     */
1209
    private static function createChildNavItem($item, $extension_handle)
1210
    {
1211
        if (!isset($item['relative']) || $item['relative'] === true) {
1212
            $link = '/extension/' . $extension_handle . '/' . ltrim($item['link'], '/');
1213
        } else {
1214
            $link = '/' . ltrim($item['link'], '/');
1215
        }
1216
1217
        $nav_item = array(
1218
            'link' => $link,
1219
            'name' => $item['name'],
1220
            'visible' => (isset($item['visible']) && $item['visible'] == 'no') ? 'no' : 'yes',
1221
            'limit' => isset($item['limit']) ? $item['limit'] : null,
1222
            'target' => isset($item['target']) ? $item['target'] : null
1223
        );
1224
1225
        return $nav_item;
1226
    }
1227
1228
    /**
1229
     * This function populates the `$_navigation` array with an associative array
1230
     * of all the navigation groups and their links. Symphony only supports one
1231
     * level of navigation, so children links cannot have children links. The default
1232
     * Symphony navigation is found in the `ASSETS/xml/navigation.xml` folder. This is
1233
     * loaded first, and then the Section navigation is built, followed by the Extension
1234
     * navigation. Additionally, this function will set the active group of the navigation
1235
     * by checking the current page against the array of links.
1236
     *
1237
     * It fires a delegate, NavigationPostBuild, to allow extensions to manipulate
1238
     * the navigation.
1239
     *
1240
     * @uses NavigationPostBuild
1241
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/assets/xml/navigation.xml
1242
     * @link https://github.com/symphonycms/symphony-2/blob/master/symphony/lib/toolkit/class.extension.php
1243
     */
1244
    public function __buildNavigation()
1245
    {
1246
        $nav = array();
1247
1248
        $this->buildXmlNavigation($nav);
1249
        $this->buildSectionNavigation($nav);
1250
        $this->buildExtensionsNavigation($nav);
1251
1252
        $pageCallback = Administration::instance()->getPageCallback();
1253
1254
        $pageRoot = $pageCallback['pageroot'] . (isset($pageCallback['context'][0]) ? $pageCallback['context'][0] . '/' : '');
1255
        $found = self::__findActiveNavigationGroup($nav, $pageRoot);
1256
1257
        // Normal searches failed. Use a regular expression using the page root. This is less
1258
        // efficient and should never really get invoked unless something weird is going on
1259
        if (!$found) {
1260
            self::__findActiveNavigationGroup($nav, '/^' . str_replace('/', '\/', $pageCallback['pageroot']) . '/i', true);
1261
        }
1262
1263
        ksort($nav);
1264
        $this->_navigation = $nav;
1265
1266
        /**
1267
         * Immediately after the navigation array as been built. Provided with the
1268
         * navigation array. Manipulating it will alter the navigation for all pages.
1269
         * Developers can also alter the 'limit' property of each page to allow more
1270
         * or less access to them.
1271
         * Preventing a user from accessing the page affects both the navigation and the
1272
         * page access rights: user will get a 403 Forbidden error if not authorized.
1273
         *
1274
         * @delegate NavigationPostBuild
1275
         * @since Symphony 2.7.0
1276
         * @param string $context
1277
         *  '/backend/'
1278
         * @param array $nav
1279
         *  An associative array of the current navigation, passed by reference
1280
         */
1281
        Symphony::ExtensionManager()->notifyMembers('NavigationPostBuild', '/backend/', array(
1282
            'navigation' => &$this->_navigation,
1283
        ));
1284
    }
1285
1286
    /**
1287
     * Given an associative array representing the navigation, and a group,
1288
     * this function will attempt to return the index of the group in the navigation
1289
     * array. If it is found, it will return the index, otherwise it will return false.
1290
     *
1291
     * @param array $nav
1292
     *  An associative array of the navigation where the key is the group
1293
     *  index, and the value is an associative array of 'name', 'index' and
1294
     *  'children'. Name is the name of the this group, index is the same as
1295
     *  the key and children is an associative array of navigation items containing
1296
     *  the keys 'link', 'name' and 'visible'. The 'haystack'.
1297
     * @param string $group
1298
     *  The group name to find, the 'needle'.
1299
     * @return integer|boolean
1300
     *  If the group is found, the index will be returned, otherwise false.
1301
     */
1302
    private static function __navigationFindGroupIndex(array $nav, $group)
1303
    {
1304
        foreach ($nav as $index => $item) {
1305
            if ($item['name'] === $group) {
1306
                return $index;
1307
            }
1308
        }
1309
1310
        return false;
1311
    }
1312
1313
    /**
1314
     * Given the navigation array, this function will loop over all the items
1315
     * to determine which is the 'active' navigation group, or in other words,
1316
     * what group best represents the current page `$this->Author` is viewing.
1317
     * This is done by checking the current page's link against all the links
1318
     * provided in the `$nav`, and then flagging the group of the found link
1319
     * with an 'active' CSS class. The current page's link omits any flags or
1320
     * URL parameters and just uses the root page URL.
1321
     *
1322
     * @param array $nav
1323
     *  An associative array of the navigation where the key is the group
1324
     *  index, and the value is an associative array of 'name', 'index' and
1325
     *  'children'. Name is the name of the this group, index is the same as
1326
     *  the key and children is an associative array of navigation items containing
1327
     *  the keys 'link', 'name' and 'visible'. The 'haystack'. This parameter is passed
1328
     *  by reference to this function.
1329
     * @param string $pageroot
1330
     *  The current page the Author is the viewing, minus any flags or URL
1331
     *  parameters such as a Symphony object ID. eg. Section ID, Entry ID. This
1332
     *  parameter is also be a regular expression, but this is highly unlikely.
1333
     * @param boolean $pattern
1334
     *  If set to true, the `$pageroot` represents a regular expression which will
1335
     *  determine if the active navigation item
1336
     * @return boolean
1337
     *  Returns true if an active link was found, false otherwise. If true, the
1338
     *  navigation group of the active link will be given the CSS class 'active'
1339
     */
1340
    private static function __findActiveNavigationGroup(array &$nav, $pageroot, $pattern = false)
1341
    {
1342
        foreach ($nav as $index => $contents) {
1343
            if (is_array($contents['children']) && !empty($contents['children'])) {
1344
                foreach ($contents['children'] as $item) {
1345
                    if ($pattern && preg_match($pageroot, $item['link'])) {
1346
                        $nav[$index]['class'] = 'active';
1347
                        return true;
1348
                    } elseif ($item['link'] == $pageroot) {
1349
                        $nav[$index]['class'] = 'active';
1350
                        return true;
1351
                    }
1352
                }
1353
            }
1354
        }
1355
1356
        return false;
1357
    }
1358
1359
    /**
1360
     * Creates the Symphony footer for an Administration page. By default
1361
     * this includes the installed Symphony version and the currently logged
1362
     * in Author. A delegate is provided to allow extensions to manipulate the
1363
     * footer HTML, which is an XMLElement of a `<ul>` element.
1364
     * Since Symphony 2.3, it no longer uses the `AddElementToFooter` delegate.
1365
     */
1366
    public function appendUserLinks()
1367
    {
1368
        $ul = new XMLElement('ul', null, array('id' => 'session'));
1369
1370
        $li = new XMLElement('li');
1371
        $li->appendChild(
1372
            Widget::Anchor(
1373
                Symphony::Author()->getFullName(),
1374
                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...
1375
            )
1376
        );
1377
        $ul->appendChild($li);
1378
1379
        $li = new XMLElement('li');
1380
        $li->appendChild(Widget::Anchor(__('Log out'), SYMPHONY_URL . '/logout/', null, null, null, array('accesskey' => 'l')));
1381
        $ul->appendChild($li);
1382
1383
        $this->Header->appendChild($ul);
1384
    }
1385
1386
    /**
1387
     * Adds a localized Alert message for failed timestamp validations.
1388
     * It also adds meta information about the last author and timestamp.
1389
     *
1390
     * @since Symphony 2.7.0
1391
     * @param string $errorMessage
1392
     *  The error message to display.
1393
     * @param Entry|Section $existingObject
1394
     *  The Entry or section object that failed validation.
1395
     * @param string $action
1396
     *  The requested action.
1397
     */
1398
    public function addTimestampValidationPageAlert($errorMessage, $existingObject, $action)
1399
    {
1400
        $authorId = $existingObject->get('modification_author_id');
1401
        if (!$authorId) {
1402
            $authorId = $existingObject->get('author_id');
1403
        }
1404
        $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

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

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