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.
Completed
Pull Request — integration (#2604)
by Brendan
05:01
created

FrontendPage   F

Complexity

Total Complexity 119

Size/Duplication

Total Lines 1039
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 10
Bugs 2 Features 0
Metric Value
c 10
b 2
f 0
dl 0
loc 1039
rs 1.0168
wmc 119
lcom 1
cbo 18

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A Env() 0 4 1
A setEnv() 0 4 1
A pageData() 0 4 1
A Page() 0 4 1
A Params() 0 4 1
F generate() 0 148 16
F __buildPage() 0 226 37
F resolvePage() 0 106 20
A __isSchemaValid() 0 6 1
A sanitizeParameter() 0 4 1
C processEvents() 0 76 8
D processDatasources() 0 105 10
C __findDatasourceOrder() 0 53 12
A __buildDatasourcePooledParamList() 0 15 4
A __findEventOrder() 0 14 4

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
    class FrontendPage extends XSLTPage
18
    {
19
        /**
20
         * An associative array of all the parameters for this page including
21
         * Symphony parameters, URL Parameters, DS Parameters and Page
22
         * parameters
23
         *
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
         *
72
         * @var array
73
         */
74
        private $_env = array();
75
76
        /**
77
         * Constructor function sets the `$is_logged_in` variable.
78
         */
79
        public function __construct()
80
        {
81
            parent::__construct();
82
83
            $this->is_logged_in = Frontend::instance()->isLoggedIn();
84
        }
85
86
        /**
87
         * Accessor function for the environment variables, aka `$this->_env`
88
         *
89
         * @return array
90
         */
91
        public function Env()
92
        {
93
            return $this->_env;
94
        }
95
96
        /**
97
         * Setter function for `$this->_env`, which takes an associative array
98
         * of environment information and replaces the existing `$this->_env`.
99
         *
100
         * @since Symphony 2.3
101
         * @param array $env
102
         *  An associative array of new environment values
103
         */
104
        public function setEnv(array $env = array())
105
        {
106
            $this->_env = $env;
107
        }
108
109
        /**
110
         * Accessor function for the resolved page's data (`$this->_pageData`)
111
         * as it lies in `tbl_pages`
112
         *
113
         * @return array
114
         */
115
        public function pageData()
116
        {
117
            return $this->_pageData;
118
        }
119
120
        /**
121
         * Accessor function for this current page URL, `$this->_page`
122
         *
123
         * @return string
124
         */
125
        public function Page()
126
        {
127
            return $this->_page;
128
        }
129
130
        /**
131
         * Accessor function for the current page params, `$this->_param`
132
         *
133
         * @since Symphony 2.3
134
         * @return array
135
         */
136
        public function Params()
137
        {
138
            return $this->_param;
139
        }
140
141
        /**
142
         * This function is called immediately from the Frontend class passing the current
143
         * URL for generation. Generate will resolve the URL to the specific page in the Symphony
144
         * and then execute all events and datasources registered to this page so that it can
145
         * be rendered. A number of delegates are fired during stages of execution for extensions
146
         * to hook into.
147
         *
148
         * @uses FrontendDevKitResolve
149
         * @uses FrontendOutputPreGenerate
150
         * @uses FrontendPreRenderHeaders
151
         * @uses FrontendOutputPostGenerate
152
         * @see  __buildPage()
153
         * @param string $page
154
         * The URL of the current page that is being Rendered as returned by getCurrentPage
155
         * @throws Exception
156
         * @throws FrontendPageNotFoundException
157
         * @throws SymphonyErrorPage
158
         * @return string
159
         * The page source after the XSLT has transformed this page's XML. This would be
160
         * exactly the same as the 'view-source' from your browser
161
         */
162
        public function generate($page = null)
163
        {
164
            $full_generate = true;
165
            $devkit = null;
166
            $output = null;
167
168
            $this->addHeaderToPage('Cache-Control', 'no-cache, must-revalidate, max-age=0');
169
            $this->addHeaderToPage('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
170
171
            if ($this->is_logged_in) {
172
                /**
173
                 * Allows a devkit object to be specified, and stop continued execution:
174
                 *
175
                 * @delegate FrontendDevKitResolve
176
                 * @param string $context
177
                 * '/frontend/'
178
                 * @param boolean $full_generate
179
                 *  Whether this page will be completely generated (ie. invoke the XSLT transform)
180
                 *  or not, by default this is true. Passed by reference
181
                 * @param mixed $devkit
182
                 *  Allows a devkit to register to this page
183
                 */
184
                Symphony::ExtensionManager()->notifyMembers('FrontendDevKitResolve', '/frontend/', array(
185
                    'full_generate' => &$full_generate,
186
                    'devkit' => &$devkit
187
                ));
188
            }
189
190
            Symphony::Profiler()->sample('Page creation started');
191
            $this->_page = $page;
192
            $this->__buildPage();
193
194
            if ($full_generate) {
195
                /**
196
                 * Immediately before generating the page. Provided with the page object, XML and XSLT
197
                 *
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)) {
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
                $this->addHeaderToPage('Access-Control-Allow-Origin', URL);
235
236
                /**
237
                 * This is just prior to the page headers being rendered, and is suitable for changing them
238
                 *
239
                 * @delegate FrontendPreRenderHeaders
240
                 * @param string $context
241
                 * '/frontend/'
242
                 */
243
                Symphony::ExtensionManager()->notifyMembers('FrontendPreRenderHeaders', '/frontend/');
244
245
                $backup_param = $this->_param;
246
                $this->_param['current-query-string'] = General::wrapInCDATA($this->_param['current-query-string']);
247
248
                // In Symphony 2.4, the XML structure stays as an object until
249
                // the very last moment.
250
                Symphony::Profiler()->seed(precision_timer());
251
                if ($this->_xml instanceof XMLElement) {
252
                    $this->setXML($this->_xml->generate(true, 0));
253
                }
254
                Symphony::Profiler()->sample('XML Generation', PROFILE_LAP);
255
256
                $output = parent::generate();
257
                $this->_param = $backup_param;
258
259
                /**
260
                 * Immediately after generating the page. Provided with string containing page source
261
                 *
262
                 * @delegate FrontendOutputPostGenerate
263
                 * @param string $context
264
                 * '/frontend/'
265
                 * @param string $output
266
                 *  The generated output of this page, ie. a string of HTML, passed by reference
267
                 */
268
                Symphony::ExtensionManager()->notifyMembers('FrontendOutputPostGenerate', '/frontend/',
269
                    array('output' => &$output));
270
271
                Symphony::Profiler()->sample('XSLT Transformation', PROFILE_LAP);
272
273
                if (is_null($devkit) && !$output) {
274
                    $errstr = null;
275
276
                    while (list($key, $val) = $this->Proc->getError()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $key is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
277
                        $errstr .= 'Line: ' . $val['line'] . ' - ' . $val['message'] . PHP_EOL;
278
                    }
279
280
                    Frontend::instance()->throwCustomError(
281
                        trim($errstr),
282
                        __('XSLT Processing Error'),
283
                        Page::HTTP_STATUS_ERROR,
284
                        'xslt',
285
                        array('proc' => clone $this->Proc)
286
                    );
287
                }
288
289
                Symphony::Profiler()->sample('Page creation complete');
290
            }
291
292
            if (!is_null($devkit)) {
293
                $devkit->prepare($this, $this->_pageData, $this->_xml, $this->_param, $output);
294
295
                return $devkit->build();
296
            }
297
298
            // Display the Event Results in the page source if the user is logged
299
            // into Symphony, the page is not JSON and if it is enabled in the
300
            // configuration.
301
            if ($this->is_logged_in && !General::in_iarray('JSON',
302
                    $this->_pageData['type']) && Symphony::Configuration()->get('display_event_xml_in_source',
303
                    'public') === 'yes'
304
            ) {
305
                $output .= PHP_EOL . '<!-- ' . PHP_EOL . $this->_events_xml->generate(true) . ' -->';
306
            }
307
308
            return $output;
309
        }
310
311
        /**
312
         * This function sets the page's parameters, processes the Datasources and
313
         * Events and sets the `$xml` and `$xsl` variables. This functions resolves the `$page`
314
         * by calling the `resolvePage()` function. If a page is not found, it attempts
315
         * to locate the Symphony 404 page set in the backend otherwise it throws
316
         * the default Symphony 404 page. If the page is found, the page's XSL utility
317
         * is found, and the system parameters are set, including any URL parameters,
318
         * params from the Symphony cookies. Events and Datasources are executed and
319
         * any parameters  generated by them are appended to the existing parameters
320
         * before setting the Page's XML and XSL variables are set to the be the
321
         * generated XML (from the Datasources and Events) and the XSLT (from the
322
         * file attached to this Page)
323
         *
324
         * @uses FrontendPageResolved
325
         * @uses FrontendParamsResolve
326
         * @uses FrontendParamsPostResolve
327
         * @see  resolvePage()
328
         */
329
        private function __buildPage()
0 ignored issues
show
Coding Style introduced by
__buildPage uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
__buildPage uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
330
        {
331
            $start = precision_timer();
332
333
            if (!$page = $this->resolvePage()) {
334
                throw new FrontendPageNotFoundException;
335
            }
336
337
            /**
338
             * Just after having resolved the page, but prior to any commencement of output creation
339
             *
340
             * @delegate FrontendPageResolved
341
             * @param string $context
342
             * '/frontend/'
343
             * @param FrontendPage $page
344
             *  An instance of this class, passed by reference
345
             * @param array $page_data
346
             *  An associative array of page data, which is a combination from `tbl_pages` and
347
             *  the path of the page on the filesystem. Passed by reference
348
             */
349
            Symphony::ExtensionManager()->notifyMembers('FrontendPageResolved', '/frontend/',
350
                array('page' => &$this, 'page_data' => &$page));
351
352
            $this->_pageData = $page;
0 ignored issues
show
Documentation Bug introduced by
It seems like $page can also be of type boolean. However, the property $_pageData is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
353
            $path = explode('/', $page['path']);
354
            $root_page = is_array($path) ? array_shift($path) : $path;
355
            $current_path = explode(dirname($_SERVER['SCRIPT_NAME']), $_SERVER['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,
376
                'workspace' => URL . '/workspace',
377
                'http-host' => HTTP_HOST,
378
                'root-page' => ($root_page ? $root_page : $page['handle']),
379
                'current-page' => $page['handle'],
380
                'current-page-id' => $page['id'],
381
                'current-path' => ($current_path === '') ? '/' : $current_path,
382
                'parent-path' => '/' . $page['path'],
383
                'current-query-string' => self::sanitizeParameter($querystring),
384
                'current-url' => URL . $current_path,
385
                'upload-limit' => min($upload_size_php, $upload_size_sym),
386
                'symphony-version' => Symphony::Configuration()->get('version', 'symphony'),
387
            );
388
389
            if (isset($this->_env['url']) && is_array($this->_env['url'])) {
390
                foreach ($this->_env['url'] as $key => $val) {
391
                    $this->_param[$key] = $val;
392
                }
393
            }
394
395
            if (is_array($_GET) && !empty($_GET)) {
396
                foreach ($_GET as $key => $val) {
397
                    if (in_array($key, array('symphony-page', 'debug', 'profile'))) {
398
                        continue;
399
                    }
400
401
                    // If the browser sends encoded entities for &, ie. a=1&amp;b=2
402
                    // this causes the $_GET to output they key as amp;b, which results in
403
                    // $url-amp;b. This pattern will remove amp; allow the correct param
404
                    // to be used, $url-b
405
                    $key = preg_replace('/(^amp;|\/)/', null, $key);
406
407
                    // If the key gets replaced out then it will break the XML so prevent
408
                    // the parameter being set.
409
                    $key = General::createHandle($key);
410
                    if (!$key) {
411
                        continue;
412
                    }
413
414
                    // Handle ?foo[bar]=hi as well as straight ?foo=hi RE: #1348
415
                    if (is_array($val)) {
416
                        $val = General::array_map_recursive(array('FrontendPage', 'sanitizeParameter'), $val);
0 ignored issues
show
Documentation introduced by
array('FrontendPage', 'sanitizeParameter') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
579
                $this->_page = $page;
580
            }
581
582
            $row = null;
583
            /**
584
             * Before page resolve. Allows manipulation of page without redirection
585
             *
586
             * @delegate FrontendPrePageResolve
587
             * @param string $context
588
             * '/frontend/'
589
             * @param mixed $row
590
             * @param FrontendPage $page
591
             *  An instance of this FrontendPage
592
             */
593
            Symphony::ExtensionManager()->notifyMembers('FrontendPrePageResolve', '/frontend/',
594
                array('row' => &$row, 'page' => &$this->_page));
595
596
            // Default to the index page if no page has been specified
597
            if ((!$this->_page || $this->_page === '//') && is_null($row)) {
598
                $row = PageManager::fetchPageByType('index');
599
600
                // Not the index page (or at least not on first impression)
601
            } elseif (is_null($row)) {
602
                $page_extra_bits = array();
603
                $pathArr = preg_split('/\//', trim($this->_page, '/'), -1, PREG_SPLIT_NO_EMPTY);
604
                $handle = array_pop($pathArr);
605
606
                do {
607
                    $path = implode('/', $pathArr);
608
609
                    if ($row = PageManager::resolvePageByPath($handle, $path)) {
0 ignored issues
show
Documentation introduced by
$path is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
610
                        $pathArr[] = $handle;
611
612
                        break 1;
613
                    } else {
614
                        $page_extra_bits[] = $handle;
615
                    }
616
                } while (($handle = array_pop($pathArr)) !== null);
617
618
                // If the `$pathArr` is empty, that means a page hasn't resolved for
619
                // the given `$page`, however in some cases the index page may allow
620
                // parameters, so we'll check.
621
                if (empty($pathArr)) {
622
                    // If the index page does not handle parameters, then return false
623
                    // (which will give up the 404), otherwise treat the `$page` as
624
                    // parameters of the index. RE: #1351
625
                    $index = PageManager::fetchPageByType('index');
626
627
                    if (!$this->__isSchemaValid($index['params'], $page_extra_bits)) {
628
                        return false;
629
                    } else {
630
                        $row = $index;
631
                    }
632
633
                    // Page resolved, check the schema (are the parameters valid?)
634
                } elseif (!$this->__isSchemaValid($row['params'], $page_extra_bits)) {
635
                    return false;
636
                }
637
            }
638
639
            // Nothing resolved, bail now
640
            if (!is_array($row) || empty($row)) {
641
                return false;
642
            }
643
644
            // Process the extra URL params
645
            $url_params = preg_split('/\//', $row['params'], -1, PREG_SPLIT_NO_EMPTY);
646
647
            foreach ($url_params as $var) {
648
                $this->_env['url'][$var] = null;
649
            }
650
651
            if (isset($page_extra_bits)) {
652
                if (!empty($page_extra_bits)) {
653
                    $page_extra_bits = array_reverse($page_extra_bits);
654
                }
655
656
                for ($i = 0, $ii = count($page_extra_bits); $i < $ii; $i++) {
657
                    $this->_env['url'][$url_params[$i]] = str_replace(' ', '+', $page_extra_bits[$i]);
658
                }
659
            }
660
661
            $row['type'] = PageManager::fetchPageTypes($row['id']);
662
663
            // Make sure the user has permission to access this page
664
            if (!$this->is_logged_in && in_array('admin', $row['type'])) {
665
                $row = PageManager::fetchPageByType('403');
666
667
                if (empty($row)) {
668
                    Frontend::instance()->throwCustomError(
669
                        __('Please login to view this page.') . ' <a href="' . SYMPHONY_URL . '/login/">' . __('Take me to the login page') . '</a>.',
670
                        __('Forbidden'),
671
                        Page::HTTP_STATUS_FORBIDDEN
672
                    );
673
                }
674
675
                $row['type'] = PageManager::fetchPageTypes($row['id']);
676
            }
677
678
            $row['filelocation'] = PageManager::resolvePageFileLocation($row['path'], $row['handle']);
679
680
            return $row;
681
        }
682
683
        /**
684
         * Given the allowed params for the resolved page, compare it to
685
         * params provided in the URL. If the number of params provided
686
         * is less than or equal to the number of URL parameters set for a page,
687
         * the Schema is considered valid, otherwise, it's considered to be false
688
         * a 404 page will result.
689
         *
690
         * @param string $schema
691
         *  The URL schema for a page, ie. article/read
692
         * @param array $bits
693
         *  The URL parameters provided from parsing the current URL. This
694
         *  does not include any `$_GET` or `$_POST` variables.
695
         * @return boolean
696
         *  True if the number of $schema (split by /) is less than the size
697
         *  of the $bits array.
698
         */
699
        private function __isSchemaValid($schema, array $bits)
700
        {
701
            $schema_arr = preg_split('/\//', $schema, -1, PREG_SPLIT_NO_EMPTY);
702
703
            return (count($schema_arr) >= count($bits));
704
        }
705
706
        /**
707
         * Given a string (expected to be a URL parameter) this function will
708
         * ensure it is safe to embed in an XML document.
709
         *
710
         * @since Symphony 2.3.1
711
         * @param string $parameter
712
         *  The string to sanitize for XML
713
         * @return string
714
         *  The sanitized string
715
         */
716
        public static function sanitizeParameter($parameter)
717
        {
718
            return XMLElement::stripInvalidXMLCharacters($parameter);
719
        }
720
721
        /**
722
         * The processEvents function executes all Events attached to the resolved
723
         * page in the correct order determined by `__findEventOrder()`. The results
724
         * from the Events are appended to the page's XML. Events execute first,
725
         * before Datasources.
726
         *
727
         * @uses FrontendProcessEvents
728
         * @uses FrontendEventPostProcess
729
         * @param string $events
730
         *  A string of all the Events attached to this page, comma separated.
731
         * @param XMLElement $wrapper
732
         *  The XMLElement to append the Events results to. Event results are
733
         *  contained in a root XMLElement that is the handlised version of
734
         *  their name.
735
         * @throws Exception
736
         */
737
        private function processEvents($events, XMLElement &$wrapper)
738
        {
739
            /**
740
             * Manipulate the events array and event element wrapper
741
             *
742
             * @delegate FrontendProcessEvents
743
             * @param string $context
744
             * '/frontend/'
745
             * @param array $env
746
             * @param string $events
747
             *  A string of all the Events attached to this page, comma separated.
748
             * @param XMLElement $wrapper
749
             *  The XMLElement to append the Events results to. Event results are
750
             *  contained in a root XMLElement that is the handlised version of
751
             *  their name.
752
             * @param array $page_data
753
             *  An associative array of page meta data
754
             */
755
            Symphony::ExtensionManager()->notifyMembers('FrontendProcessEvents', '/frontend/', array(
756
                'env' => $this->_env,
757
                'events' => &$events,
758
                'wrapper' => &$wrapper,
759
                'page_data' => $this->_pageData
760
            ));
761
762
            if (strlen(trim($events)) > 0) {
763
                $events = preg_split('/,\s*/i', $events, -1, PREG_SPLIT_NO_EMPTY);
764
                $events = array_map('trim', $events);
765
766
                if (!is_array($events) || empty($events)) {
767
                    return;
768
                }
769
770
                $pool = array();
771
772
                foreach ($events as $handle) {
773
                    $pool[$handle] = EventManager::create($handle,
774
                        array('env' => $this->_env, 'param' => $this->_param));
775
                }
776
777
                uasort($pool, array($this, '__findEventOrder'));
778
779
                foreach ($pool as $handle => $event) {
780
                    $startTime = precision_timer();
781
                    $queries = Symphony::Database()->queryCount();
782
783
                    if ($xml = $event->load()) {
784
                        if (is_object($xml)) {
785
                            $wrapper->appendChild($xml);
786
                        } else {
787
                            $wrapper->setValue(
788
                                $wrapper->getValue() . PHP_EOL . '    ' . trim($xml)
789
                            );
790
                        }
791
                    }
792
793
                    $queries = Symphony::Database()->queryCount() - $queries;
794
                    Symphony::Profiler()->seed($startTime);
795
                    Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Event', $queries);
796
                }
797
            }
798
799
            /**
800
             * Just after the page events have triggered. Provided with the XML object
801
             *
802
             * @delegate FrontendEventPostProcess
803
             * @param string $context
804
             * '/frontend/'
805
             * @param XMLElement $xml
806
             *  The XMLElement to append the Events results to. Event results are
807
             *  contained in a root XMLElement that is the handlised version of
808
             *  their name.
809
             */
810
            Symphony::ExtensionManager()->notifyMembers('FrontendEventPostProcess', '/frontend/',
811
                array('xml' => &$wrapper));
812
        }
813
814
        /**
815
         * Given an array of all the Datasources for this page, sort them into the
816
         * correct execution order and append the Datasource results to the
817
         * page XML. If the Datasource provides any parameters, they will be
818
         * added to the `$env` pool for use by other Datasources and eventual
819
         * inclusion into the page parameters.
820
         *
821
         * @param string $datasources
822
         *  A string of Datasource's attached to this page, comma separated.
823
         * @param XMLElement $wrapper
824
         *  The XMLElement to append the Datasource results to. Datasource
825
         *  results are contained in a root XMLElement that is the handlised
826
         *  version of their name.
827
         * @param array $params
828
         *  Any params to automatically add to the `$env` pool, by default this
829
         *  is an empty array. It looks like Symphony does not utilise this parameter
830
         *  at all
831
         * @throws Exception
832
         */
833
        public function processDatasources($datasources, XMLElement &$wrapper, array $params = array())
834
        {
835
            if (trim($datasources) === '') {
836
                return;
837
            }
838
839
            $datasources = preg_split('/,\s*/i', $datasources, -1, PREG_SPLIT_NO_EMPTY);
840
            $datasources = array_map('trim', $datasources);
841
842
            if (!is_array($datasources) || empty($datasources)) {
843
                return;
844
            }
845
846
            $this->_env['pool'] = $params;
847
            $pool = $params;
848
            $dependencies = array();
849
850
            foreach ($datasources as $handle) {
851
                $pool[$handle] = DatasourceManager::create($handle, array(), false);
852
                $dependencies[$handle] = $pool[$handle]->getDependencies();
853
            }
854
855
            $dsOrder = $this->__findDatasourceOrder($dependencies);
856
857
            foreach ($dsOrder as $handle) {
858
                $startTime = precision_timer();
859
                $queries = Symphony::Database()->queryCount();
860
861
                // default to no XML
862
                $xml = null;
863
                $ds = $pool[$handle];
864
865
                // Handle redirect on empty setting correctly RE: #1539
866
                try {
867
                    $ds->processParameters(array('env' => $this->_env, 'param' => $this->_param));
868
                } catch (FrontendPageNotFoundException $e) {
869
                    // Work around. This ensures the 404 page is displayed and
870
                    // is not picked up by the default catch() statement below
871
                    FrontendPageNotFoundExceptionHandler::render($e);
872
                }
873
874
                /**
875
                 * Allows extensions to execute the data source themselves (e.g. for caching)
876
                 * and providing their own output XML instead
877
                 *
878
                 * @since Symphony 2.3
879
                 * @delegate DataSourcePreExecute
880
                 * @param string $context
881
                 * '/frontend/'
882
                 * @param DataSource $datasource
883
                 *  The Datasource object
884
                 * @param mixed $xml
885
                 *  The XML output of the data source. Can be an `XMLElement` or string.
886
                 * @param array $param_pool
887
                 *  The existing param pool including output parameters of any previous data sources
888
                 */
889
                Symphony::ExtensionManager()->notifyMembers('DataSourcePreExecute', '/frontend/', array(
890
                    'datasource' => &$ds,
891
                    'xml' => &$xml,
892
                    'param_pool' => &$this->_env['pool']
893
                ));
894
895
                // if the XML is still null, an extension has not run the data source, so run normally
896
                if (is_null($xml)) {
897
                    $xml = $ds->execute($this->_env['pool']);
898
                }
899
900
                if ($xml) {
901
                    /**
902
                     * After the datasource has executed, either by itself or via the
903
                     * `DataSourcePreExecute` delegate, and if the `$xml` variable is truthy,
904
                     * this delegate allows extensions to modify the output XML and parameter pool
905
                     *
906
                     * @since Symphony 2.3
907
                     * @delegate DataSourcePostExecute
908
                     * @param string $context
909
                     * '/frontend/'
910
                     * @param DataSource $datasource
911
                     *  The Datasource object
912
                     * @param mixed $xml
913
                     *  The XML output of the data source. Can be an `XMLElement` or string.
914
                     * @param array $param_pool
915
                     *  The existing param pool including output parameters of any previous data sources
916
                     */
917
                    Symphony::ExtensionManager()->notifyMembers('DataSourcePostExecute', '/frontend/', array(
918
                        'datasource' => $ds,
919
                        'xml' => &$xml,
920
                        'param_pool' => &$this->_env['pool']
921
                    ));
922
923
                    if ($xml instanceof XMLElement) {
924
                        $wrapper->appendChild($xml);
925
                    } else {
926
                        $wrapper->appendChild(
927
                            '    ' . trim($xml) . PHP_EOL
928
                        );
929
                    }
930
                }
931
932
                $queries = Symphony::Database()->queryCount() - $queries;
933
                Symphony::Profiler()->seed($startTime);
934
                Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Datasource', $queries);
935
                unset($ds);
936
            }
937
        }
938
939
        /**
940
         * The function finds the correct order Datasources need to be processed in to
941
         * satisfy all dependencies that parameters can resolve correctly and in time for
942
         * other Datasources to filter on.
943
         *
944
         * @param array $dependenciesList
945
         *  An associative array with the key being the Datasource handle and the values
946
         *  being it's dependencies.
947
         * @return array
948
         *  The sorted array of Datasources in order of how they should be executed
949
         */
950
        private function __findDatasourceOrder($dependenciesList)
951
        {
952
            if (!is_array($dependenciesList) || empty($dependenciesList)) {
953
                return array();
954
            }
955
956
            foreach ($dependenciesList as $handle => $dependencies) {
957
                foreach ($dependencies as $i => $dependency) {
958
                    $dependency = explode('.', $dependency);
959
                    $dependenciesList[$handle][$i] = reset($dependency);
960
                }
961
            }
962
963
            $orderedList = array();
964
            $dsKeyArray = $this->__buildDatasourcePooledParamList(array_keys($dependenciesList));
965
966
            // 1. First do a cleanup of each dependency list, removing non-existant DS's and find
967
            //    the ones that have no dependencies, removing them from the list
968
            foreach ($dependenciesList as $handle => $dependencies) {
969
                $dependenciesList[$handle] = @array_intersect($dsKeyArray, $dependencies);
970
971
                if (empty($dependenciesList[$handle])) {
972
                    unset($dependenciesList[$handle]);
973
                    $orderedList[] = str_replace('_', '-', $handle);
974
                }
975
            }
976
977
            // 2. Iterate over the remaining DS's. Find if all their dependencies are
978
            //    in the $orderedList array. Keep iterating until all DS's are in that list
979
            //    or there are circular dependencies (list doesn't change between iterations
980
            //    of the while loop)
981
            do {
982
                $last_count = count($dependenciesList);
983
984
                foreach ($dependenciesList as $handle => $dependencies) {
985
                    if (General::in_array_all(array_map(function ($a) {
986
                        return str_replace('$ds-', '', $a);
987
                    }, $dependencies), $orderedList)
988
                    ) {
989
                        $orderedList[] = str_replace('_', '-', $handle);
990
                        unset($dependenciesList[$handle]);
991
                    }
992
                }
993
            } while (!empty($dependenciesList) && $last_count > count($dependenciesList));
994
995
            if (!empty($dependenciesList)) {
996
                $orderedList = array_merge($orderedList, array_keys($dependenciesList));
997
            }
998
999
            return array_map(function ($a) {
1000
                return str_replace('-', '_', $a);
1001
            }, $orderedList);
1002
        }
1003
1004
        /**
1005
         * Given an array of datasource dependancies, this function will translate
1006
         * each of them to be a valid datasource handle.
1007
         *
1008
         * @param array $datasources
1009
         *  The datasource dependencies
1010
         * @return array
1011
         *  An array of the handlised datasources
1012
         */
1013
        private function __buildDatasourcePooledParamList($datasources)
1014
        {
1015
            if (!is_array($datasources) || empty($datasources)) {
1016
                return array();
1017
            }
1018
1019
            $list = array();
1020
1021
            foreach ($datasources as $handle) {
1022
                $rootelement = str_replace('_', '-', $handle);
1023
                $list[] = '$ds-' . $rootelement;
1024
            }
1025
1026
            return $list;
1027
        }
1028
1029
        /**
1030
         * This function determines the correct order that events should be executed in.
1031
         * Events are executed based off priority, with `Event::kHIGH` priority executing
1032
         * first. If there is more than one Event of the same priority, they are then
1033
         * executed in alphabetical order. This function is designed to be used with
1034
         * PHP's uasort function.
1035
         *
1036
         * @link http://php.net/manual/en/function.uasort.php
1037
         * @param Event $a
1038
         * @param Event $b
1039
         * @return integer
1040
         */
1041
        private function __findEventOrder($a, $b)
1042
        {
1043
            if ($a->priority() === $b->priority()) {
1044
                $a = $a->about();
0 ignored issues
show
Bug introduced by
The method about() does not seem to exist on object<Event>.

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...
1045
                $b = $b->about();
0 ignored issues
show
Bug introduced by
The method about() does not seem to exist on object<Event>.

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...
1046
1047
                $handles = array($a['name'], $b['name']);
1048
                asort($handles);
1049
1050
                return (key($handles) === 0) ? -1 : 1;
1051
            }
1052
1053
            return (($a->priority() > $b->priority()) ? -1 : 1);
1054
        }
1055
    }
1056