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.

FrontendPage   F
last analyzed

Complexity

Total Complexity 118

Size/Duplication

Total Lines 1017
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 1
Metric Value
eloc 349
c 10
b 0
f 1
dl 0
loc 1017
rs 2
wmc 118

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setEnv() 0 3 1
A Env() 0 3 1
A pageData() 0 3 1
A Params() 0 3 1
A Page() 0 3 1
A sanitizeParameter() 0 3 1
B processDatasources() 0 104 10
B __findDatasourceOrder() 0 47 11
D generate() 0 144 16
A __findEventOrder() 0 12 4
A __buildDatasourcePooledParamList() 0 14 4
F __buildPage() 0 221 37
A __construct() 0 5 1
F resolvePage() 0 103 20
B processEvents() 0 71 8
A __isSchemaValid() 0 5 1

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.

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

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

594
            $handle = array_pop(/** @scrutinizer ignore-type */ $pathArr);
Loading history...
595
596
            do {
597
                $path = implode('/', $pathArr);
598
599
                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

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

693
        return (count(/** @scrutinizer ignore-type */ $schema_arr) >= count($bits));
Loading history...
694
    }
695
696
    /**
697
     * The processEvents function executes all Events attached to the resolved
698
     * page in the correct order determined by `__findEventOrder()`. The results
699
     * from the Events are appended to the page's XML. Events execute first,
700
     * before Datasources.
701
     *
702
     * @uses FrontendProcessEvents
703
     * @uses FrontendEventPostProcess
704
     * @param string $events
705
     *  A string of all the Events attached to this page, comma separated.
706
     * @param XMLElement $wrapper
707
     *  The XMLElement to append the Events results to. Event results are
708
     *  contained in a root XMLElement that is the handlised version of
709
     *  their name.
710
     * @throws Exception
711
     */
712
    private function processEvents($events, XMLElement &$wrapper)
713
    {
714
        /**
715
         * Manipulate the events array and event element wrapper
716
         * @delegate FrontendProcessEvents
717
         * @param string $context
718
         * '/frontend/'
719
         * @param array $env
720
         * @param string $events
721
         *  A string of all the Events attached to this page, comma separated.
722
         * @param XMLElement $wrapper
723
         *  The XMLElement to append the Events results to. Event results are
724
         *  contained in a root XMLElement that is the handlised version of
725
         *  their name.
726
         * @param array $page_data
727
         *  An associative array of page meta data
728
         */
729
        Symphony::ExtensionManager()->notifyMembers('FrontendProcessEvents', '/frontend/', array(
730
            'env' => $this->_env,
731
            'events' => &$events,
732
            'wrapper' => &$wrapper,
733
            'page_data' => $this->_pageData
734
        ));
735
736
        if (strlen(trim($events)) > 0) {
737
            $events = preg_split('/,\s*/i', $events, -1, PREG_SPLIT_NO_EMPTY);
738
            $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

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

799
        if ($a->/** @scrutinizer ignore-call */ priority() == $b->priority()) {

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...
800
            $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

800
            /** @scrutinizer ignore-call */ 
801
            $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...
801
            $b = $b->about();
802
803
            $handles = array($a['name'], $b['name']);
804
            asort($handles);
805
806
            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 integer|null|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...
807
        }
0 ignored issues
show
Coding Style introduced by
No blank line found after control structure
Loading history...
808
        return $a->priority() > $b->priority() ? -1 : 1;
809
    }
810
811
    /**
812
     * Given an array of all the Datasources for this page, sort them into the
813
     * correct execution order and append the Datasource results to the
814
     * page XML. If the Datasource provides any parameters, they will be
815
     * added to the `$env` pool for use by other Datasources and eventual
816
     * inclusion into the page parameters.
817
     *
818
     * @param string $datasources
819
     *  A string of Datasource's attached to this page, comma separated.
820
     * @param XMLElement $wrapper
821
     *  The XMLElement to append the Datasource results to. Datasource
822
     *  results are contained in a root XMLElement that is the handlised
823
     *  version of their name.
824
     * @param array $params
825
     *  Any params to automatically add to the `$env` pool, by default this
826
     *  is an empty array. It looks like Symphony does not utilise this parameter
827
     *  at all
828
     * @throws Exception
829
     */
830
    public function processDatasources($datasources, XMLElement &$wrapper, array $params = array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$params" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$params"; expected 0 but found 1
Loading history...
831
    {
832
        if (trim($datasources) == '') {
833
            return;
834
        }
835
836
        $datasources = preg_split('/,\s*/i', $datasources, -1, PREG_SPLIT_NO_EMPTY);
837
        $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

837
        $datasources = array_map('trim', /** @scrutinizer ignore-type */ $datasources);
Loading history...
838
839
        if (!is_array($datasources) || empty($datasources)) {
0 ignored issues
show
introduced by
The condition is_array($datasources) is always true.
Loading history...
840
            return;
841
        }
842
843
        $this->_env['pool'] = $params;
844
        $pool = $params;
845
        $dependencies = array();
846
847
        foreach ($datasources as $handle) {
848
            $pool[$handle] = DatasourceManager::create($handle, array(), false);
849
            $dependencies[$handle] = $pool[$handle]->getDependencies();
850
        }
851
852
        $dsOrder = $this->__findDatasourceOrder($dependencies);
853
854
        foreach ($dsOrder as $handle) {
855
            $startTime = precision_timer();
856
            $queries = Symphony::Database()->queryCount();
857
858
            // default to no XML
859
            $xml = null;
860
            $ds = $pool[$handle];
861
862
            // Handle redirect on empty setting correctly RE: #1539
863
            try {
864
                $ds->processParameters(array('env' => $this->_env, 'param' => $this->_param));
865
            } catch (FrontendPageNotFoundException $e) {
866
                // Work around. This ensures the 404 page is displayed and
867
                // is not picked up by the default catch() statement below
868
                FrontendPageNotFoundExceptionHandler::render($e);
869
            }
870
871
            /**
872
             * Allows extensions to execute the data source themselves (e.g. for caching)
873
             * and providing their own output XML instead
874
             *
875
             * @since Symphony 2.3
876
             * @delegate DataSourcePreExecute
877
             * @param string $context
878
             * '/frontend/'
879
             * @param DataSource $datasource
880
             *  The Datasource object
881
             * @param mixed $xml
882
             *  The XML output of the data source. Can be an `XMLElement` or string.
883
             * @param array $param_pool
884
             *  The existing param pool including output parameters of any previous data sources
885
             */
886
            Symphony::ExtensionManager()->notifyMembers('DataSourcePreExecute', '/frontend/', array(
887
                'datasource' => &$ds,
888
                'xml' => &$xml,
889
                'param_pool' => &$this->_env['pool']
890
            ));
891
892
            // if the XML is still null, an extension has not run the data source, so run normally
893
            // This is deprecated and will be replaced by execute in Symphony 3.0.0
894
            if (is_null($xml)) {
895
                $xml = $ds->grab($this->_env['pool']);
896
            }
897
898
            if ($xml) {
899
                /**
900
                 * After the datasource has executed, either by itself or via the
901
                 * `DataSourcePreExecute` delegate, and if the `$xml` variable is truthy,
902
                 * this delegate allows extensions to modify the output XML and parameter pool
903
                 *
904
                 * @since Symphony 2.3
905
                 * @delegate DataSourcePostExecute
906
                 * @param string $context
907
                 * '/frontend/'
908
                 * @param DataSource $datasource
909
                 *  The Datasource object
910
                 * @param mixed $xml
911
                 *  The XML output of the data source. Can be an `XMLElement` or string.
912
                 * @param array $param_pool
913
                 *  The existing param pool including output parameters of any previous data sources
914
                 */
915
                Symphony::ExtensionManager()->notifyMembers('DataSourcePostExecute', '/frontend/', array(
916
                    'datasource' => $ds,
917
                    'xml' => &$xml,
918
                    'param_pool' => &$this->_env['pool']
919
                ));
920
921
                if ($xml instanceof XMLElement) {
922
                    $wrapper->appendChild($xml);
923
                } else {
924
                    $wrapper->appendChild(
925
                        '    ' . trim($xml) . PHP_EOL
926
                    );
927
                }
928
            }
929
930
            $queries = Symphony::Database()->queryCount() - $queries;
931
            Symphony::Profiler()->seed($startTime);
932
            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...
933
            unset($ds);
934
        }
935
    }
936
937
    /**
938
     * The function finds the correct order Datasources need to be processed in to
939
     * satisfy all dependencies that parameters can resolve correctly and in time for
940
     * other Datasources to filter on.
941
     *
942
     * @param array $dependenciesList
943
     *  An associative array with the key being the Datasource handle and the values
944
     *  being it's dependencies.
945
     * @return array
946
     *  The sorted array of Datasources in order of how they should be executed
947
     */
948
    private function __findDatasourceOrder($dependenciesList)
949
    {
950
        if (!is_array($dependenciesList) || empty($dependenciesList)) {
0 ignored issues
show
introduced by
The condition is_array($dependenciesList) is always true.
Loading history...
951
            return;
952
        }
953
954
        foreach ($dependenciesList as $handle => $dependencies) {
955
            foreach ($dependencies as $i => $dependency) {
956
                $dependency = explode('.', $dependency);
957
                $dependenciesList[$handle][$i] = reset($dependency);
958
            }
959
        }
960
961
        $orderedList = array();
962
        $dsKeyArray = $this->__buildDatasourcePooledParamList(array_keys($dependenciesList));
963
964
        // 1. First do a cleanup of each dependency list, removing non-existant DS's and find
965
        //    the ones that have no dependencies, removing them from the list
966
        foreach ($dependenciesList as $handle => $dependencies) {
967
            $dependenciesList[$handle] = @array_intersect($dsKeyArray, $dependencies);
968
969
            if (empty($dependenciesList[$handle])) {
970
                unset($dependenciesList[$handle]);
971
                $orderedList[] = str_replace('_', '-', $handle);
972
            }
973
        }
974
975
        // 2. Iterate over the remaining DS's. Find if all their dependencies are
976
        //    in the $orderedList array. Keep iterating until all DS's are in that list
977
        //    or there are circular dependencies (list doesn't change between iterations
978
        //    of the while loop)
979
        do {
980
            $last_count = count($dependenciesList);
981
982
            foreach ($dependenciesList as $handle => $dependencies) {
983
                if (General::in_array_all(array_map(function($a) {return str_replace('$ds-', '', $a);}, $dependencies), $orderedList)) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
Coding Style introduced by
Opening brace must be the last content on the line
Loading history...
984
                    $orderedList[] = str_replace('_', '-', $handle);
985
                    unset($dependenciesList[$handle]);
986
                }
987
            }
988
        } while (!empty($dependenciesList) && $last_count > count($dependenciesList));
989
990
        if (!empty($dependenciesList)) {
991
            $orderedList = array_merge($orderedList, array_keys($dependenciesList));
992
        }
993
994
        return array_map(function($a) {return str_replace('-', '_', $a);}, $orderedList);
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
Coding Style introduced by
Opening brace must be the last content on the line
Loading history...
Coding Style introduced by
It is generally recommended to place each PHP statement on a line by itself.

Let’s take a look at an example:

// Bad
$a = 5; $b = 6; $c = 7;

// Good
$a = 5;
$b = 6;
$c = 7;
Loading history...
995
    }
996
997
    /**
998
     * Given an array of datasource dependancies, this function will translate
999
     * each of them to be a valid datasource handle.
1000
     *
1001
     * @param array $datasources
1002
     *  The datasource dependencies
1003
     * @return array
1004
     *  An array of the handlised datasources
1005
     */
1006
    private function __buildDatasourcePooledParamList($datasources)
1007
    {
1008
        if (!is_array($datasources) || empty($datasources)) {
0 ignored issues
show
introduced by
The condition is_array($datasources) is always true.
Loading history...
1009
            return array();
1010
        }
1011
1012
        $list = array();
1013
1014
        foreach ($datasources as $handle) {
1015
            $rootelement = str_replace('_', '-', $handle);
1016
            $list[] = '$ds-' . $rootelement;
1017
        }
1018
1019
        return $list;
1020
    }
1021
1022
    /**
1023
     * Given a string (expected to be a URL parameter) this function will
1024
     * ensure it is safe to embed in an XML document.
1025
     *
1026
     * @since Symphony 2.3.1
1027
     * @param string $parameter
1028
     *  The string to sanitize for XML
1029
     * @return string
1030
     *  The sanitized string
1031
     */
1032
    public static function sanitizeParameter($parameter)
1033
    {
1034
        return XMLElement::stripInvalidXMLCharacters($parameter);
1035
    }
1036
}
1037