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

FrontendPage   F

Complexity

Total Complexity 119

Size/Duplication

Total Lines 1019
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
dl 0
loc 1019
rs 1.1431
c 0
b 0
f 0
wmc 119
lcom 1
cbo 18

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setEnv() 0 3 1
C processDatasources() 0 109 12
C __findDatasourceOrder() 0 47 11
A Env() 0 3 1
A pageData() 0 3 1
A Params() 0 3 1
A __findEventOrder() 0 12 4
A __buildDatasourcePooledParamList() 0 14 4
A Page() 0 3 1
A __construct() 0 5 1
F resolvePage() 0 103 20
C processEvents() 0 71 8
A __isSchemaValid() 0 5 1
F __buildPage() 0 226 39
F generate() 0 144 16

How to fix   Complexity   

Complex Class

Complex classes like FrontendPage 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 FrontendPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package toolkit
5
 */
6
7
/**
8
 * The `FrontendPage` class represents a page of the website that is powered
9
 * by Symphony. It takes the current URL and resolves it to a page as specified
10
 * in Symphony which involves deducing the parameters from the URL, ensuring
11
 * this page is accessible and exists, setting the correct Content-Type for the page
12
 * and executing any Datasources or Events attached to the page to generate a
13
 * string of HTML that is returned to the browser. If the resolved page does not exist
14
 * or the user is not allowed to view it, the appropriate 404/403 page will be shown
15
 * instead.
16
 */
17
18
class FrontendPage extends XSLTPage
19
{
20
    /**
21
     * An associative array of all the parameters for this page including
22
     * Symphony parameters, URL Parameters, DS Parameters and Page
23
     * parameters
24
     * @var array
25
     */
26
    public $_param = array();
27
28
    /**
29
     * The URL of the current page that is being Rendered as returned
30
     * by `getCurrentPage`
31
     *
32
     * @var string
33
     * @see boot#getCurrentPage()
34
     */
35
    private $_page;
36
37
    /**
38
     * An associative array of the resolved pages's data as returned from `tbl_pages`
39
     * with the keys mapping to the columns in that table. Additionally, 'file-location'
40
     * and 'type' are also added to this array
41
     *
42
     * @var array
43
     */
44
    private $_pageData;
45
46
    /**
47
     * Returns whether the user accessing this page is logged in as a Symphony
48
     * Author
49
     *
50
     * @since Symphony 2.2.1
51
     * @var boolean
52
     */
53
    private $is_logged_in = false;
54
55
    /**
56
     * When events are processed, the results of them often can't be reproduced
57
     * when debugging the page as they happen during `$_POST`. There is a Symphony
58
     * configuration setting that allows the event results to be appended as a HTML
59
     * comment at the bottom of the page source, so logged in Authors can view-source
60
     * page to see the result of an event. This variable holds the event XML so that it
61
     * can be appended to the page source if `display_event_xml_in_source` is set to 'yes'.
62
     * By default this is set to no.
63
     *
64
     * @var XMLElement
65
     */
66
    private $_events_xml;
67
68
    /**
69
     * Holds all the environment variables which include parameters set by
70
     * other Datasources or Events.
71
     * @var array
72
     */
73
    private $_env = array();
74
75
    /**
76
     * Hold all the data sources that must not output their parameters in the xml.
77
     * @var array
78
     */
79
    private $_xml_excluded_params = array();
80
81
    /**
82
     * Constructor function sets the `$is_logged_in` variable.
83
     */
84
    public function __construct()
85
    {
86
        parent::__construct();
87
88
        $this->is_logged_in = Frontend::instance()->isLoggedIn();
89
    }
90
91
    /**
92
     * Accessor function for the environment variables, aka `$this->_env`
93
     *
94
     * @return array
95
     */
96
    public function Env()
97
    {
98
        return $this->_env;
99
    }
100
101
    /**
102
     * Setter function for `$this->_env`, which takes an associative array
103
     * of environment information and replaces the existing `$this->_env`.
104
     *
105
     * @since Symphony 2.3
106
     * @param array $env
107
     *  An associative array of new environment values
108
     */
109
    public function setEnv(array $env = array())
110
    {
111
        $this->_env = $env;
112
    }
113
114
    /**
115
     * Accessor function for the resolved page's data (`$this->_pageData`)
116
     * as it lies in `tbl_pages`
117
     *
118
     * @return array
119
     */
120
    public function pageData()
121
    {
122
        return $this->_pageData;
123
    }
124
125
    /**
126
     * Accessor function for this current page URL, `$this->_page`
127
     *
128
     * @return string
129
     */
130
    public function Page()
131
    {
132
        return $this->_page;
133
    }
134
135
    /**
136
     * Accessor function for the current page params, `$this->_param`
137
     *
138
     * @since Symphony 2.3
139
     * @return array
140
     */
141
    public function Params()
142
    {
143
        return $this->_param;
144
    }
145
146
    /**
147
     * This function is called immediately from the Frontend class passing the current
148
     * URL for generation. Generate will resolve the URL to the specific page in the Symphony
149
     * and then execute all events and datasources registered to this page so that it can
150
     * be rendered. A number of delegates are fired during stages of execution for extensions
151
     * to hook into.
152
     *
153
     * @uses FrontendDevKitResolve
154
     * @uses FrontendOutputPreGenerate
155
     * @uses FrontendPreRenderHeaders
156
     * @uses FrontendOutputPostGenerate
157
     * @see __buildPage()
158
     * @param string $page
159
     * The URL of the current page that is being Rendered as returned by getCurrentPage
160
     * @throws Exception
161
     * @throws FrontendPageNotFoundException
162
     * @throws SymphonyErrorPage
163
     * @return string
164
     * The page source after the XSLT has transformed this page's XML. This would be
165
     * exactly the same as the 'view-source' from your browser
166
     */
167
    public function generate($page = null)
168
    {
169
        $full_generate = true;
170
        $devkit = null;
171
        $output = null;
172
173
        $this->addHeaderToPage('Cache-Control', 'no-cache, must-revalidate, max-age=0');
174
        $this->addHeaderToPage('Expires', 'Mon, 12 Dec 1982 06:14:00 GMT');
175
        $this->addHeaderToPage('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
176
        $this->addHeaderToPage('Pragma', 'no-cache');
177
178
        if ($this->is_logged_in) {
179
            /**
180
             * Allows a devkit object to be specified, and stop continued execution:
181
             *
182
             * @delegate FrontendDevKitResolve
183
             * @param string $context
184
             * '/frontend/'
185
             * @param boolean $full_generate
186
             *  Whether this page will be completely generated (ie. invoke the XSLT transform)
187
             *  or not, by default this is true. Passed by reference
188
             * @param mixed $devkit
189
             *  Allows a devkit to register to this page
190
             */
191
            Symphony::ExtensionManager()->notifyMembers('FrontendDevKitResolve', '/frontend/', array(
192
                'full_generate' => &$full_generate,
193
                'devkit'        => &$devkit
194
            ));
195
        }
196
197
        Symphony::Profiler()->sample('Page creation started');
198
        $this->_page = $page;
199
        $this->__buildPage();
200
201
        if ($full_generate) {
0 ignored issues
show
introduced by
The condition $full_generate is always true.
Loading history...
202
            /**
203
             * Immediately before generating the page. Provided with the page object, XML and XSLT
204
             * @delegate FrontendOutputPreGenerate
205
             * @param string $context
206
             * '/frontend/'
207
             * @param FrontendPage $page
208
             *  This FrontendPage object, by reference
209
             * @param XMLElement $xml
210
             *  This pages XML, including the Parameters, Datasource and Event XML, by reference as
211
             *  an XMLElement
212
             * @param string $xsl
213
             *  This pages XSLT, by reference
214
             */
215
            Symphony::ExtensionManager()->notifyMembers('FrontendOutputPreGenerate', '/frontend/', array(
216
                'page'  => &$this,
217
                'xml'   => &$this->_xml,
218
                'xsl'   => &$this->_xsl
219
            ));
220
221
            if (is_null($devkit)) {
0 ignored issues
show
introduced by
The condition is_null($devkit) is always true.
Loading history...
222
                if (General::in_iarray('XML', $this->_pageData['type'])) {
223
                    $this->addHeaderToPage('Content-Type', 'text/xml; charset=utf-8');
224
                } elseif (General::in_iarray('JSON', $this->_pageData['type'])) {
225
                    $this->addHeaderToPage('Content-Type', 'application/json; charset=utf-8');
226
                } else {
227
                    $this->addHeaderToPage('Content-Type', 'text/html; charset=utf-8');
228
                }
229
230
                if (in_array('404', $this->_pageData['type'])) {
231
                    $this->setHttpStatus(self::HTTP_STATUS_NOT_FOUND);
232
                } elseif (in_array('403', $this->_pageData['type'])) {
233
                    $this->setHttpStatus(self::HTTP_STATUS_FORBIDDEN);
234
                }
235
            }
236
237
            // Lock down the frontend first so that extensions can easily remove these
238
            // headers if desired. RE: #2480
239
            $this->addHeaderToPage('X-Frame-Options', 'SAMEORIGIN');
240
            // Add more http security headers, RE: #2248
241
            $this->addHeaderToPage('X-Content-Type-Options', 'nosniff');
242
            $this->addHeaderToPage('X-XSS-Protection', '1; mode=block');
243
244
            /**
245
             * This is just prior to the page headers being rendered, and is suitable for changing them
246
             * @delegate FrontendPreRenderHeaders
247
             * @param string $context
248
             * '/frontend/'
249
             */
250
            Symphony::ExtensionManager()->notifyMembers('FrontendPreRenderHeaders', '/frontend/');
251
252
            $backup_param = $this->_param;
253
            $this->_param['current-query-string'] = General::wrapInCDATA($this->_param['current-query-string']);
254
255
            // In Symphony 2.4, the XML structure stays as an object until
256
            // the very last moment.
257
            Symphony::Profiler()->seed(precision_timer());
258
            if ($this->_xml instanceof XMLElement) {
259
                $this->setXML($this->_xml->generate(true, 0));
260
            }
261
            Symphony::Profiler()->sample('XML Generation', 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...
262
263
            $output = parent::generate();
264
            $this->_param = $backup_param;
265
266
            Symphony::Profiler()->sample('XSLT Transformation', PROFILE_LAP);
267
268
            /**
269
             * Immediately after generating the page. Provided with string containing page source
270
             * @delegate FrontendOutputPostGenerate
271
             * @param string $context
272
             * '/frontend/'
273
             * @param string $output
274
             *  The generated output of this page, ie. a string of HTML, passed by reference
275
             */
276
            Symphony::ExtensionManager()->notifyMembers('FrontendOutputPostGenerate', '/frontend/', array('output' => &$output));
277
278
            if (is_null($devkit) && !$output) {
279
                $errstr = null;
280
281
                while (list(, $val) = $this->Proc->getError()) {
282
                    $errstr .= 'Line: ' . $val['line'] . ' - ' . $val['message'] . PHP_EOL;
283
                }
284
285
                Frontend::instance()->throwCustomError(
286
                    trim($errstr),
287
                    __('XSLT Processing Error'),
288
                    Page::HTTP_STATUS_ERROR,
289
                    'xslt',
290
                    array('proc' => clone $this->Proc)
291
                );
292
            }
293
294
            Symphony::Profiler()->sample('Page creation complete');
295
        }
296
297
        if (!is_null($devkit)) {
298
            $devkit->prepare($this, $this->_pageData, $this->_xml, $this->_param, $output);
299
300
            return $devkit->build();
301
        }
302
303
        // Display the Event Results in the page source if the user is logged
304
        // into Symphony, the page is not JSON and if it is enabled in the
305
        // configuration.
306
        if ($this->is_logged_in && !General::in_iarray('JSON', $this->_pageData['type']) && Symphony::Configuration()->get('display_event_xml_in_source', 'public') === 'yes') {
307
            $output .= PHP_EOL . '<!-- ' . PHP_EOL . $this->_events_xml->generate(true) . ' -->';
308
        }
309
310
        return $output;
311
    }
312
313
    /**
314
     * This function sets the page's parameters, processes the Datasources and
315
     * Events and sets the `$xml` and `$xsl` variables. This functions resolves the `$page`
316
     * by calling the `resolvePage()` function. If a page is not found, it attempts
317
     * to locate the Symphony 404 page set in the backend otherwise it throws
318
     * the default Symphony 404 page. If the page is found, the page's XSL utility
319
     * is found, and the system parameters are set, including any URL parameters,
320
     * params from the Symphony cookies. Events and Datasources are executed and
321
     * any parameters  generated by them are appended to the existing parameters
322
     * before setting the Page's XML and XSL variables are set to the be the
323
     * generated XML (from the Datasources and Events) and the XSLT (from the
324
     * file attached to this Page)
325
     *
326
     * @uses FrontendPageResolved
327
     * @uses FrontendParamsResolve
328
     * @uses FrontendParamsPostResolve
329
     * @see resolvePage()
330
     */
331
    private function __buildPage()
332
    {
333
        $start = precision_timer();
334
335
        if (!$page = $this->resolvePage()) {
336
            throw new FrontendPageNotFoundException;
337
        }
338
339
        /**
340
         * Just after having resolved the page, but prior to any commencement of output creation
341
         * @delegate FrontendPageResolved
342
         * @param string $context
343
         * '/frontend/'
344
         * @param FrontendPage $page
345
         *  An instance of this class, passed by reference
346
         * @param array $page_data
347
         *  An associative array of page data, which is a combination from `tbl_pages` and
348
         *  the path of the page on the filesystem. Passed by reference
349
         */
350
        Symphony::ExtensionManager()->notifyMembers('FrontendPageResolved', '/frontend/', array('page' => &$this, 'page_data' => &$page));
351
352
        $this->_pageData = $page;
353
        $path = explode('/', $page['path']);
354
        $root_page = is_array($path) ? array_shift($path) : $path;
0 ignored issues
show
introduced by
The condition is_array($path) is always true.
Loading history...
355
        $current_path = explode(dirname(server_safe('SCRIPT_NAME')), server_safe('REQUEST_URI'), 2);
356
        $current_path = '/' . ltrim(end($current_path), '/');
357
        $split_path = explode('?', $current_path, 3);
358
        $current_path = rtrim(current($split_path), '/');
359
        $querystring = next($split_path);
360
361
        // Get max upload size from php and symphony config then choose the smallest
362
        $upload_size_php = ini_size_to_bytes(ini_get('upload_max_filesize'));
363
        $upload_size_sym = Symphony::Configuration()->get('max_upload_size', 'admin');
364
        $date = new DateTime();
365
366
        $this->_param = array(
367
            'today' => $date->format('Y-m-d'),
368
            'current-time' => $date->format('H:i'),
369
            'this-year' => $date->format('Y'),
370
            'this-month' => $date->format('m'),
371
            'this-day' => $date->format('d'),
372
            'timezone' => $date->format('P'),
373
            'website-name' => Symphony::Configuration()->get('sitename', 'general'),
374
            'page-title' => $page['title'],
375
            '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...
376
            'workspace' => URL . '/workspace',
377
            'workspace-path' => DIRROOT . '/workspace',
0 ignored issues
show
Bug introduced by
The constant DIRROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
378
            'http-host' => HTTP_HOST,
0 ignored issues
show
Bug introduced by
The constant HTTP_HOST was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
379
            'root-page' => ($root_page ? $root_page : $page['handle']),
380
            'current-page' => $page['handle'],
381
            'current-page-id' => $page['id'],
382
            'current-path' => ($current_path == '') ? '/' : $current_path,
383
            'parent-path' => '/' . $page['path'],
384
            'current-query-string' => self::sanitizeParameter($querystring),
385
            'current-url' => URL . $current_path,
386
            'upload-limit' => min($upload_size_php, $upload_size_sym),
387
            'symphony-version' => Symphony::Configuration()->get('version', 'symphony'),
388
        );
389
390
        if (isset($this->_env['url']) && is_array($this->_env['url'])) {
391
            foreach ($this->_env['url'] as $key => $val) {
392
                $this->_param[$key] = $val;
393
            }
394
        }
395
396
        if (is_array($_GET) && !empty($_GET)) {
397
            foreach ($_GET as $key => $val) {
398
                if (in_array($key, array('symphony-page', 'debug', 'profile'))) {
399
                    continue;
400
                }
401
402
                // If the browser sends encoded entities for &, ie. a=1&amp;b=2
403
                // this causes the $_GET to output they key as amp;b, which results in
404
                // $url-amp;b. This pattern will remove amp; allow the correct param
405
                // to be used, $url-b
406
                $key = preg_replace('/(^amp;|\/)/', null, $key);
407
408
                // If the key gets replaced out then it will break the XML so prevent
409
                // the parameter being set.
410
                $key = General::createHandle($key);
411
                if (!$key) {
412
                    continue;
413
                }
414
415
                // Handle ?foo[bar]=hi as well as straight ?foo=hi RE: #1348
416
                if (is_array($val)) {
417
                    $val = General::array_map_recursive(array('FrontendPage', 'sanitizeParameter'), $val);
0 ignored issues
show
Bug introduced by
array('FrontendPage', 'sanitizeParameter') of type array<integer,string> is incompatible with the type string expected by parameter $function of General::array_map_recursive(). ( Ignorable by Annotation )

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

417
                    $val = General::array_map_recursive(/** @scrutinizer ignore-type */ array('FrontendPage', 'sanitizeParameter'), $val);
Loading history...
418
                } else {
419
                    $val = self::sanitizeParameter($val);
420
                }
421
422
                $this->_param['url-' . $key] = $val;
423
            }
424
        }
425
426
        if (is_array($_COOKIE[__SYM_COOKIE_PREFIX__]) && !empty($_COOKIE[__SYM_COOKIE_PREFIX__])) {
0 ignored issues
show
Bug introduced by
The constant __SYM_COOKIE_PREFIX__ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
427
            foreach ($_COOKIE[__SYM_COOKIE_PREFIX__] as $key => $val) {
428
                if ($key === 'xsrf-token' && is_array($val)) {
429
                    $val = key($val);
430
                }
431
432
                $this->_param['cookie-' . $key] = $val;
433
            }
434
        }
435
436
        // Flatten parameters:
437
        General::flattenArray($this->_param);
438
439
        // Add Page Types to parameters so they are not flattened too early
440
        $this->_param['page-types'] = $page['type'];
441
442
        // Add Page events the same way
443
        $this->_param['page-events'] = explode(',', trim(str_replace('_', '-', $page['events']), ','));
444
445
        /**
446
         * Just after having resolved the page params, but prior to any commencement of output creation
447
         * @delegate FrontendParamsResolve
448
         * @param string $context
449
         * '/frontend/'
450
         * @param array $params
451
         *  An associative array of this page's parameters
452
         */
453
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsResolve', '/frontend/', array('params' => &$this->_param));
454
455
        $xml_build_start = precision_timer();
456
457
        $xml = new XMLDocument('data');
458
        $xml->renderHeader();
459
460
        $events = new XMLElement('events');
461
        $this->processEvents($page['events'], $events);
462
        $xml->appendChild($events);
463
464
        $this->_events_xml = clone $events;
465
466
        $this->processDatasources($page['data_sources'], $xml);
467
468
        Symphony::Profiler()->seed($xml_build_start);
469
        Symphony::Profiler()->sample('XML 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...
470
471
        if (isset($this->_env['pool']) && is_array($this->_env['pool']) && !empty($this->_env['pool'])) {
472
            foreach ($this->_env['pool'] as $handle => $p) {
473
                if (!is_array($p)) {
474
                    $p = array($p);
475
                }
476
477
                // Check if the data source is excluded from xml output
478
                $dsName = current(explode('.', $handle));
479
                if ($dsName && $this->_xml_excluded_params[$dsName]) {
480
                    continue;
481
                }
482
483
                foreach ($p as $key => $value) {
484
                    if (is_array($value) && !empty($value)) {
485
                        foreach ($value as $kk => $vv) {
486
                            $this->_param[$handle] .= @implode(', ', $vv) . ',';
487
                        }
488
                    } else {
489
                        $this->_param[$handle] = @implode(', ', $p);
490
                    }
491
                }
492
493
                $this->_param[$handle] = trim($this->_param[$handle], ',');
494
            }
495
        }
496
497
        /**
498
         * Access to the resolved param pool, including additional parameters provided by Data Source outputs
499
         * @delegate FrontendParamsPostResolve
500
         * @param string $context
501
         * '/frontend/'
502
         * @param array $params
503
         *  An associative array of this page's parameters
504
         */
505
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsPostResolve', '/frontend/', array('params' => &$this->_param));
506
507
        $params = new XMLElement('params');
508
        foreach ($this->_param as $key => $value) {
509
            // To support multiple parameters using the 'datasource.field'
510
            // we will pop off the field handle prior to sanitizing the
511
            // key. This is because of a limitation where General::createHandle
512
            // will strip '.' as it's technically punctuation.
513
            if (strpos($key, '.') !== false) {
514
                $parts = explode('.', $key);
515
                $field_handle = '.' . array_pop($parts);
516
                $key = implode('', $parts);
517
            } else {
518
                $field_handle = '';
519
            }
520
521
            $key = Lang::createHandle($key) . $field_handle;
522
            $param = new XMLElement($key);
523
524
            // DS output params get flattened to a string, so get the original pre-flattened array
525
            if (isset($this->_env['pool'][$key])) {
526
                $value = $this->_env['pool'][$key];
527
            }
528
529
            if (is_array($value) && !(count($value) == 1 && empty($value[0]))) {
530
                foreach ($value as $key => $value) {
0 ignored issues
show
Comprehensibility Bug introduced by
$value is overwriting a variable from outer foreach loop.
Loading history...
Comprehensibility Bug introduced by
$key is overwriting a variable from outer foreach loop.
Loading history...
531
                    $item = new XMLElement('item', General::sanitize($value));
532
                    $item->setAttribute('handle', Lang::createHandle($value));
533
                    $param->appendChild($item);
534
                }
535
            } elseif (is_array($value)) {
536
                $param->setValue(General::sanitize($value[0]));
537
            } elseif (in_array($key, array('xsrf-token', 'current-query-string'))) {
538
                $param->setValue(General::wrapInCDATA($value));
539
            } else {
540
                $param->setValue(General::sanitize($value));
541
            }
542
543
            $params->appendChild($param);
544
        }
545
        $xml->prependChild($params);
546
547
        $this->setXML($xml);
548
        $xsl = '<?xml version="1.0" encoding="UTF-8"?>' .
549
               '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' .
550
               '    <xsl:import href="/' . rawurlencode(ltrim($page['filelocation'], '/')) . '"/>' .
551
               '</xsl:stylesheet>';
552
553
        $this->setXSL($xsl, false);
554
555
        Symphony::Profiler()->seed($start);
556
        Symphony::Profiler()->sample('Page Built', PROFILE_LAP);
557
    }
558
559
    /**
560
     * This function attempts to resolve the given page in to it's Symphony page. If no
561
     * page is given, it is assumed the 'index' is being requested. Before a page row is
562
     * returned, it is checked to see that if it has the 'admin' type, that the requesting
563
     * user is authenticated as a Symphony author. If they are not, the Symphony 403
564
     * page is returned (whether that be set as a user defined page using the page type
565
     * of 403, or just returning the Default Symphony 403 error page). Any URL parameters
566
     * set on the page are added to the `$env` variable before the function returns an
567
     * associative array of page details such as Title, Content Type etc.
568
     *
569
     * @uses FrontendPrePageResolve
570
     * @see __isSchemaValid()
571
     * @param string $page
572
     * The URL of the current page that is being Rendered as returned by `getCurrentPage()`.
573
     * If no URL is provided, Symphony assumes the Page with the type 'index' is being
574
     * requested.
575
     * @throws SymphonyErrorPage
576
     * @return array
577
     *  An associative array of page details
578
     */
579
    public function resolvePage($page = null)
580
    {
581
        if ($page) {
582
            $this->_page = $page;
583
        }
584
585
        $row = null;
586
        /**
587
         * Before page resolve. Allows manipulation of page without redirection
588
         * @delegate FrontendPrePageResolve
589
         * @param string $context
590
         * '/frontend/'
591
         * @param mixed $row
592
         * @param FrontendPage $page
593
         *  An instance of this FrontendPage
594
         */
595
        Symphony::ExtensionManager()->notifyMembers('FrontendPrePageResolve', '/frontend/', array('row' => &$row, 'page' => &$this->_page));
596
597
        // Default to the index page if no page has been specified
598
        if ((!$this->_page || $this->_page == '//') && is_null($row)) {
599
            $row = PageManager::fetchPageByType('index');
600
601
            // Not the index page (or at least not on first impression)
602
        } elseif (is_null($row)) {
603
            $page_extra_bits = array();
604
            $pathArr = preg_split('/\//', trim($this->_page, '/'), -1, PREG_SPLIT_NO_EMPTY);
605
            $handle = array_pop($pathArr);
0 ignored issues
show
Bug introduced by
It seems like $pathArr can also be of type false; however, parameter $array of array_pop() does only seem to accept 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

605
            $handle = array_pop(/** @scrutinizer ignore-type */ $pathArr);
Loading history...
606
607
            do {
608
                $path = implode('/', $pathArr);
609
610
                if ($row = PageManager::resolvePageByPath($handle, $path)) {
0 ignored issues
show
Bug introduced by
$path of type string is incompatible with the type boolean expected by parameter $path of PageManager::resolvePageByPath(). ( Ignorable by Annotation )

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

610
                if ($row = PageManager::resolvePageByPath($handle, /** @scrutinizer ignore-type */ $path)) {
Loading history...
611
                    $pathArr[] = $handle;
612
613
                    break 1;
614
                } else {
615
                    $page_extra_bits[] = $handle;
616
                }
617
            } while (($handle = array_pop($pathArr)) !== null);
618
619
            // If the `$pathArr` is empty, that means a page hasn't resolved for
620
            // the given `$page`, however in some cases the index page may allow
621
            // parameters, so we'll check.
622
            if (empty($pathArr)) {
623
                // If the index page does not handle parameters, then return false
624
                // (which will give up the 404), otherwise treat the `$page` as
625
                // parameters of the index. RE: #1351
626
                $index = PageManager::fetchPageByType('index');
627
628
                if (!$this->__isSchemaValid($index['params'], $page_extra_bits)) {
629
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
630
                } else {
631
                    $row = $index;
632
                }
633
634
                // Page resolved, check the schema (are the parameters valid?)
635
            } elseif (!$this->__isSchemaValid($row['params'], $page_extra_bits)) {
636
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
637
            }
638
        }
639
640
        // Nothing resolved, bail now
641
        if (!is_array($row) || empty($row)) {
642
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
643
        }
644
645
        // Process the extra URL params
646
        $url_params = preg_split('/\//', $row['params'], -1, PREG_SPLIT_NO_EMPTY);
647
648
        foreach ($url_params as $var) {
649
            $this->_env['url'][$var] = null;
650
        }
651
652
        if (isset($page_extra_bits)) {
653
            if (!empty($page_extra_bits)) {
654
                $page_extra_bits = array_reverse($page_extra_bits);
655
            }
656
657
            for ($i = 0, $ii = count($page_extra_bits); $i < $ii; $i++) {
658
                $this->_env['url'][$url_params[$i]] = str_replace(' ', '+', $page_extra_bits[$i]);
659
            }
660
        }
661
662
        $row['type'] = PageManager::fetchPageTypes($row['id']);
663
664
        // Make sure the user has permission to access this page
665
        if (!$this->is_logged_in && in_array('admin', $row['type'])) {
666
            $row = PageManager::fetchPageByType('403');
667
668
            if (empty($row)) {
669
                Frontend::instance()->throwCustomError(
670
                    __('Please login to view this page.') . ' <a href="' . SYMPHONY_URL . '/login/">' . __('Take me to the login page') . '</a>.',
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...
671
                    __('Forbidden'),
672
                    Page::HTTP_STATUS_FORBIDDEN
673
                );
674
            }
675
676
            $row['type'] = PageManager::fetchPageTypes($row['id']);
677
        }
678
679
        $row['filelocation'] = PageManager::resolvePageFileLocation($row['path'], $row['handle']);
680
681
        return $row;
682
    }
683
684
    /**
685
     * Given the allowed params for the resolved page, compare it to
686
     * params provided in the URL. If the number of params provided
687
     * is less than or equal to the number of URL parameters set for a page,
688
     * the Schema is considered valid, otherwise, it's considered to be false
689
     * a 404 page will result.
690
     *
691
     * @param string $schema
692
     *  The URL schema for a page, ie. article/read
693
     * @param array $bits
694
     *  The URL parameters provided from parsing the current URL. This
695
     *  does not include any `$_GET` or `$_POST` variables.
696
     * @return boolean
697
     *  true if the number of $schema (split by /) is less than the size
698
     *  of the $bits array.
699
     */
700
    private function __isSchemaValid($schema, array $bits)
701
    {
702
        $schema_arr = preg_split('/\//', $schema, -1, PREG_SPLIT_NO_EMPTY);
703
704
        return (count($schema_arr) >= count($bits));
0 ignored issues
show
Bug introduced by
It seems like $schema_arr can also be of type false; however, parameter $var of count() does only seem to accept Countable|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

704
        return (count(/** @scrutinizer ignore-type */ $schema_arr) >= count($bits));
Loading history...
705
    }
706
707
    /**
708
     * The processEvents function executes all Events attached to the resolved
709
     * page in the correct order determined by `__findEventOrder()`. The results
710
     * from the Events are appended to the page's XML. Events execute first,
711
     * before Datasources.
712
     *
713
     * @uses FrontendProcessEvents
714
     * @uses FrontendEventPostProcess
715
     * @param string $events
716
     *  A string of all the Events attached to this page, comma separated.
717
     * @param XMLElement $wrapper
718
     *  The XMLElement to append the Events results to. Event results are
719
     *  contained in a root XMLElement that is the handlised version of
720
     *  their name.
721
     * @throws Exception
722
     */
723
    private function processEvents($events, XMLElement &$wrapper)
724
    {
725
        /**
726
         * Manipulate the events array and event element wrapper
727
         * @delegate FrontendProcessEvents
728
         * @param string $context
729
         * '/frontend/'
730
         * @param array $env
731
         * @param string $events
732
         *  A string of all the Events attached to this page, comma separated.
733
         * @param XMLElement $wrapper
734
         *  The XMLElement to append the Events results to. Event results are
735
         *  contained in a root XMLElement that is the handlised version of
736
         *  their name.
737
         * @param array $page_data
738
         *  An associative array of page meta data
739
         */
740
        Symphony::ExtensionManager()->notifyMembers('FrontendProcessEvents', '/frontend/', array(
741
            'env' => $this->_env,
742
            'events' => &$events,
743
            'wrapper' => &$wrapper,
744
            'page_data' => $this->_pageData
745
        ));
746
747
        if (strlen(trim($events)) > 0) {
748
            $events = preg_split('/,\s*/i', $events, -1, PREG_SPLIT_NO_EMPTY);
749
            $events = array_map('trim', $events);
0 ignored issues
show
Bug introduced by
It seems like $events can also be of type false; however, parameter $arr1 of array_map() does only seem to accept 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

749
            $events = array_map('trim', /** @scrutinizer ignore-type */ $events);
Loading history...
750
751
            if (!is_array($events) || empty($events)) {
0 ignored issues
show
introduced by
The condition is_array($events) is always true.
Loading history...
752
                return;
753
            }
754
755
            $pool = array();
756
757
            foreach ($events as $handle) {
758
                $pool[$handle] = EventManager::create($handle, array('env' => $this->_env, 'param' => $this->_param));
759
            }
760
761
            uasort($pool, array($this, '__findEventOrder'));
762
763
            foreach ($pool as $handle => $event) {
764
                $startTime = precision_timer();
765
                $queries = Symphony::Database()->queryCount();
766
767
                if ($xml = $event->load()) {
768
                    if (is_object($xml)) {
769
                        $wrapper->appendChild($xml);
770
                    } else {
771
                        $wrapper->setValue(
772
                            $wrapper->getValue() . PHP_EOL . '    ' . trim($xml)
773
                        );
774
                    }
775
                }
776
777
                $queries = Symphony::Database()->queryCount() - $queries;
778
                Symphony::Profiler()->seed($startTime);
779
                Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Event', $queries);
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...
780
            }
781
        }
782
783
        /**
784
         * Just after the page events have triggered. Provided with the XML object
785
         * @delegate FrontendEventPostProcess
786
         * @param string $context
787
         * '/frontend/'
788
         * @param XMLElement $xml
789
         *  The XMLElement to append the Events results to. Event results are
790
         *  contained in a root XMLElement that is the handlised version of
791
         *  their name.
792
         */
793
        Symphony::ExtensionManager()->notifyMembers('FrontendEventPostProcess', '/frontend/', array('xml' => &$wrapper));
794
    }
795
796
    /**
797
     * This function determines the correct order that events should be executed in.
798
     * Events are executed based off priority, with `Event::kHIGH` priority executing
799
     * first. If there is more than one Event of the same priority, they are then
800
     * executed in alphabetical order. This function is designed to be used with
801
     * PHP's uasort function.
802
     *
803
     * @link http://php.net/manual/en/function.uasort.php
804
     * @param Event $a
805
     * @param Event $b
806
     * @return integer
807
     */
808
    private function __findEventOrder($a, $b)
809
    {
810
        if ($a->priority() == $b->priority()) {
811
            $a = $a->about();
0 ignored issues
show
Bug introduced by
The method about() does not exist on Event. ( Ignorable by Annotation )

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

811
            /** @scrutinizer ignore-call */ 
812
            $a = $a->about();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
812
            $b = $b->about();
813
814
            $handles = array($a['name'], $b['name']);
815
            asort($handles);
816
817
            return (key($handles) == 0) ? -1 : 1;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing key($handles) of type null|integer|string to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
818
        }
819
        return $a->priority() > $b->priority() ? -1 : 1;
820
    }
821
822
    /**
823
     * Given an array of all the Datasources for this page, sort them into the
824
     * correct execution order and append the Datasource results to the
825
     * page XML. If the Datasource provides any parameters, they will be
826
     * added to the `$env` pool for use by other Datasources and eventual
827
     * inclusion into the page parameters.
828
     *
829
     * @param string $datasources
830
     *  A string of Datasource's attached to this page, comma separated.
831
     * @param XMLElement $wrapper
832
     *  The XMLElement to append the Datasource results to. Datasource
833
     *  results are contained in a root XMLElement that is the handlised
834
     *  version of their name.
835
     * @param array $params
836
     *  Any params to automatically add to the `$env` pool, by default this
837
     *  is an empty array. It looks like Symphony does not utilise this parameter
838
     *  at all
839
     * @throws Exception
840
     */
841
    public function processDatasources($datasources, XMLElement &$wrapper, array $params = array())
842
    {
843
        if (trim($datasources) == '') {
844
            return;
845
        }
846
847
        $datasources = preg_split('/,\s*/i', $datasources, -1, PREG_SPLIT_NO_EMPTY);
848
        $datasources = array_map('trim', $datasources);
0 ignored issues
show
Bug introduced by
It seems like $datasources can also be of type false; however, parameter $arr1 of array_map() does only seem to accept 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

848
        $datasources = array_map('trim', /** @scrutinizer ignore-type */ $datasources);
Loading history...
849
850
        if (!is_array($datasources) || empty($datasources)) {
0 ignored issues
show
introduced by
The condition is_array($datasources) is always true.
Loading history...
851
            return;
852
        }
853
854
        $this->_env['pool'] = $params;
855
        $pool = $params;
856
        $dependencies = array();
857
858
        foreach ($datasources as $handle) {
859
            $pool[$handle] = DatasourceManager::create($handle, array(), false);
860
            $dependencies[$handle] = $pool[$handle]->getDependencies();
861
        }
862
863
        $dsOrder = $this->__findDatasourceOrder($dependencies);
864
865
        foreach ($dsOrder as $handle) {
866
            $startTime = precision_timer();
867
            $queries = Symphony::Database()->queryCount();
868
869
            // default to no XML
870
            $xml = null;
871
            $ds = $pool[$handle];
872
873
            // Handle redirect on empty setting correctly RE: #1539
874
            try {
875
                $ds->processParameters(array('env' => $this->_env, 'param' => $this->_param));
876
            } catch (FrontendPageNotFoundException $e) {
877
                // Work around. This ensures the 404 page is displayed and
878
                // is not picked up by the default catch() statement below
879
                FrontendPageNotFoundExceptionHandler::render($e);
880
            }
881
882
            /**
883
             * Allows extensions to execute the data source themselves (e.g. for caching)
884
             * and providing their own output XML instead
885
             *
886
             * @since Symphony 2.3
887
             * @delegate DataSourcePreExecute
888
             * @param string $context
889
             * '/frontend/'
890
             * @param DataSource $datasource
891
             *  The Datasource object
892
             * @param mixed $xml
893
             *  The XML output of the data source. Can be an `XMLElement` or string.
894
             * @param array $param_pool
895
             *  The existing param pool including output parameters of any previous data sources
896
             */
897
            Symphony::ExtensionManager()->notifyMembers('DataSourcePreExecute', '/frontend/', array(
898
                'datasource' => &$ds,
899
                'xml' => &$xml,
900
                'param_pool' => &$this->_env['pool']
901
            ));
902
903
            // if the XML is still null, an extension has not run the data source, so run normally
904
            // This is deprecated and will be replaced by execute in Symphony 3.0.0
905
            if (is_null($xml)) {
906
                $xml = $ds->execute($this->_env['pool']);
907
            }
908
909
            // If the data source does not want to output its xml, keep the info for later
910
            if (isset($ds->dsParamPARAMXML) && $ds->dsParamPARAMXML !== 'yes') {
911
                $this->_xml_excluded_params['ds-' . $ds->dsParamROOTELEMENT] = true;
912
            }
913
914
            if ($xml) {
915
                /**
916
                 * After the datasource has executed, either by itself or via the
917
                 * `DataSourcePreExecute` delegate, and if the `$xml` variable is truthy,
918
                 * this delegate allows extensions to modify the output XML and parameter pool
919
                 *
920
                 * @since Symphony 2.3
921
                 * @delegate DataSourcePostExecute
922
                 * @param string $context
923
                 * '/frontend/'
924
                 * @param DataSource $datasource
925
                 *  The Datasource object
926
                 * @param mixed $xml
927
                 *  The XML output of the data source. Can be an `XMLElement` or string.
928
                 * @param array $param_pool
929
                 *  The existing param pool including output parameters of any previous data sources
930
                 */
931
                Symphony::ExtensionManager()->notifyMembers('DataSourcePostExecute', '/frontend/', array(
932
                    'datasource' => $ds,
933
                    'xml' => &$xml,
934
                    'param_pool' => &$this->_env['pool']
935
                ));
936
937
                if ($xml instanceof XMLElement) {
938
                    $wrapper->appendChild($xml);
939
                } else {
940
                    $wrapper->appendChild(
941
                        '    ' . trim($xml) . PHP_EOL
942
                    );
943
                }
944
            }
945
946
            $queries = Symphony::Database()->queryCount() - $queries;
947
            Symphony::Profiler()->seed($startTime);
948
            Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Datasource', $queries);
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...
949
            unset($ds);
950
        }
951
    }
952
953
    /**
954
     * The function finds the correct order Datasources need to be processed in to
955
     * satisfy all dependencies that parameters can resolve correctly and in time for
956
     * other Datasources to filter on.
957
     *
958
     * @param array $dependenciesList
959
     *  An associative array with the key being the Datasource handle and the values
960
     *  being it's dependencies.
961
     * @return array
962
     *  The sorted array of Datasources in order of how they should be executed
963
     */
964
    private function __findDatasourceOrder($dependenciesList)
965
    {
966
        if (!is_array($dependenciesList) || empty($dependenciesList)) {
0 ignored issues
show
introduced by
The condition is_array($dependenciesList) is always true.
Loading history...
967
            return;
968
        }
969
970
        foreach ($dependenciesList as $handle => $dependencies) {
971
            foreach ($dependencies as $i => $dependency) {
972
                $dependency = explode('.', $dependency);
973
                $dependenciesList[$handle][$i] = reset($dependency);
974
            }
975
        }
976
977
        $orderedList = array();
978
        $dsKeyArray = $this->__buildDatasourcePooledParamList(array_keys($dependenciesList));
979
980
        // 1. First do a cleanup of each dependency list, removing non-existant DS's and find
981
        //    the ones that have no dependencies, removing them from the list
982
        foreach ($dependenciesList as $handle => $dependencies) {
983
            $dependenciesList[$handle] = @array_intersect($dsKeyArray, $dependencies);
984
985
            if (empty($dependenciesList[$handle])) {
986
                unset($dependenciesList[$handle]);
987
                $orderedList[] = str_replace('_', '-', $handle);
988
            }
989
        }
990
991
        // 2. Iterate over the remaining DS's. Find if all their dependencies are
992
        //    in the $orderedList array. Keep iterating until all DS's are in that list
993
        //    or there are circular dependencies (list doesn't change between iterations
994
        //    of the while loop)
995
        do {
996
            $last_count = count($dependenciesList);
997
998
            foreach ($dependenciesList as $handle => $dependencies) {
999
                if (General::in_array_all(array_map(function($a) {return str_replace('$ds-', '', $a);}, $dependencies), $orderedList)) {
1000
                    $orderedList[] = str_replace('_', '-', $handle);
1001
                    unset($dependenciesList[$handle]);
1002
                }
1003
            }
1004
        } while (!empty($dependenciesList) && $last_count > count($dependenciesList));
1005
1006
        if (!empty($dependenciesList)) {
1007
            $orderedList = array_merge($orderedList, array_keys($dependenciesList));
1008
        }
1009
1010
        return array_map(function($a) {return str_replace('-', '_', $a);}, $orderedList);
1011
    }
1012
1013
    /**
1014
     * Given an array of datasource dependancies, this function will translate
1015
     * each of them to be a valid datasource handle.
1016
     *
1017
     * @param array $datasources
1018
     *  The datasource dependencies
1019
     * @return array
1020
     *  An array of the handlised datasources
1021
     */
1022
    private function __buildDatasourcePooledParamList($datasources)
1023
    {
1024
        if (!is_array($datasources) || empty($datasources)) {
0 ignored issues
show
introduced by
The condition is_array($datasources) is always true.
Loading history...
1025
            return array();
1026
        }
1027
1028
        $list = array();
1029
1030
        foreach ($datasources as $handle) {
1031
            $rootelement = str_replace('_', '-', $handle);
1032
            $list[] = '$ds-' . $rootelement;
1033
        }
1034
1035
        return $list;
1036
    }
1037
1038
    /**
1039
     * Given a string (expected to be a URL parameter) this function will
1040
     * ensure it is safe to embed in an XML document.
1041
     *
1042
     * @since Symphony 2.3.1
1043
     * @param string $parameter
1044
     *  The string to sanitize for XML
1045
     * @return string
1046
     *  The sanitized string
1047
     */
1048
    public static function sanitizeParameter($parameter)
1049
    {
1050
        return XMLElement::stripInvalidXMLCharacters($parameter);
1051
    }
1052
}
1053