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
Push — integration ( 45cc9f...98bc42 )
by Brendan
05:52
created

FrontendPage::__buildPage()   F

Complexity

Conditions 37
Paths 2177

Size

Total Lines 221
Code Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 2
Metric Value
cc 37
eloc 118
c 6
b 2
f 2
nc 2177
nop 0
dl 0
loc 221
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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())
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)
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('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
169
170
        if ($this->is_logged_in) {
171
            /**
172
             * Allows a devkit object to be specified, and stop continued execution:
173
             *
174
             * @delegate FrontendDevKitResolve
175
             * @param string $context
176
             * '/frontend/'
177
             * @param boolean $full_generate
178
             *  Whether this page will be completely generated (ie. invoke the XSLT transform)
179
             *  or not, by default this is true. Passed by reference
180
             * @param mixed $devkit
181
             *  Allows a devkit to register to this page
182
             */
183
            Symphony::ExtensionManager()->notifyMembers('FrontendDevKitResolve', '/frontend/', array(
184
                'full_generate' => &$full_generate,
185
                'devkit'        => &$devkit
186
            ));
187
        }
188
189
        Symphony::Profiler()->sample('Page creation started');
190
        $this->_page = $page;
191
        $this->__buildPage();
192
193
        if ($full_generate) {
194
            /**
195
             * Immediately before generating the page. Provided with the page object, XML and XSLT
196
             * @delegate FrontendOutputPreGenerate
197
             * @param string $context
198
             * '/frontend/'
199
             * @param FrontendPage $page
200
             *  This FrontendPage object, by reference
201
             * @param XMLElement $xml
202
             *  This pages XML, including the Parameters, Datasource and Event XML, by reference as
203
             *  an XMLElement
204
             * @param string $xsl
205
             *  This pages XSLT, by reference
206
             */
207
            Symphony::ExtensionManager()->notifyMembers('FrontendOutputPreGenerate', '/frontend/', array(
208
                'page'  => &$this,
209
                'xml'   => &$this->_xml,
210
                'xsl'   => &$this->_xsl
211
            ));
212
213
            if (is_null($devkit)) {
214
                if (General::in_iarray('XML', $this->_pageData['type'])) {
215
                    $this->addHeaderToPage('Content-Type', 'text/xml; charset=utf-8');
216
                } elseif (General::in_iarray('JSON', $this->_pageData['type'])) {
217
                    $this->addHeaderToPage('Content-Type', 'application/json; charset=utf-8');
218
                } else {
219
                    $this->addHeaderToPage('Content-Type', 'text/html; charset=utf-8');
220
                }
221
222
                if (in_array('404', $this->_pageData['type'])) {
223
                    $this->setHttpStatus(self::HTTP_STATUS_NOT_FOUND);
224
                } elseif (in_array('403', $this->_pageData['type'])) {
225
                    $this->setHttpStatus(self::HTTP_STATUS_FORBIDDEN);
226
                }
227
            }
228
229
            // Lock down the frontend first so that extensions can easily remove these
230
            // headers if desired. RE: #2480
231
            $this->addHeaderToPage('X-Frame-Options', 'SAMEORIGIN');
232
            $this->addHeaderToPage('Access-Control-Allow-Origin', URL);
233
234
            /**
235
             * This is just prior to the page headers being rendered, and is suitable for changing them
236
             * @delegate FrontendPreRenderHeaders
237
             * @param string $context
238
             * '/frontend/'
239
             */
240
            Symphony::ExtensionManager()->notifyMembers('FrontendPreRenderHeaders', '/frontend/');
241
242
            $backup_param = $this->_param;
243
            $this->_param['current-query-string'] = General::wrapInCDATA($this->_param['current-query-string']);
244
245
            // In Symphony 2.4, the XML structure stays as an object until
246
            // the very last moment.
247
            Symphony::Profiler()->seed(precision_timer());
248
            if ($this->_xml instanceof XMLElement) {
249
                $this->setXML($this->_xml->generate(true, 0));
250
            }
251
            Symphony::Profiler()->sample('XML Generation', PROFILE_LAP);
252
253
            $output = parent::generate();
254
            $this->_param = $backup_param;
255
256
            /**
257
             * Immediately after generating the page. Provided with string containing page source
258
             * @delegate FrontendOutputPostGenerate
259
             * @param string $context
260
             * '/frontend/'
261
             * @param string $output
262
             *  The generated output of this page, ie. a string of HTML, passed by reference
263
             */
264
            Symphony::ExtensionManager()->notifyMembers('FrontendOutputPostGenerate', '/frontend/', array('output' => &$output));
265
266
            Symphony::Profiler()->sample('XSLT Transformation', PROFILE_LAP);
267
268
            if (is_null($devkit) && !$output) {
269
                $errstr = null;
270
271
                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...
272
                    $errstr .= 'Line: ' . $val['line'] . ' - ' . $val['message'] . PHP_EOL;
273
                }
274
275
                Frontend::instance()->throwCustomError(
276
                    trim($errstr),
277
                    __('XSLT Processing Error'),
278
                    Page::HTTP_STATUS_ERROR,
279
                    'xslt',
280
                    array('proc' => clone $this->Proc)
281
                );
282
            }
283
284
            Symphony::Profiler()->sample('Page creation complete');
285
        }
286
287
        if (!is_null($devkit)) {
288
            $devkit->prepare($this, $this->_pageData, $this->_xml, $this->_param, $output);
289
290
            return $devkit->build();
291
        }
292
293
        // Display the Event Results in the page source if the user is logged
294
        // into Symphony, the page is not JSON and if it is enabled in the
295
        // configuration.
296
        if ($this->is_logged_in && !General::in_iarray('JSON', $this->_pageData['type']) && Symphony::Configuration()->get('display_event_xml_in_source', 'public') === 'yes') {
297
            $output .= PHP_EOL . '<!-- ' . PHP_EOL . $this->_events_xml->generate(true) . ' -->';
298
        }
299
300
        return $output;
301
    }
302
303
    /**
304
     * This function sets the page's parameters, processes the Datasources and
305
     * Events and sets the `$xml` and `$xsl` variables. This functions resolves the `$page`
306
     * by calling the `resolvePage()` function. If a page is not found, it attempts
307
     * to locate the Symphony 404 page set in the backend otherwise it throws
308
     * the default Symphony 404 page. If the page is found, the page's XSL utility
309
     * is found, and the system parameters are set, including any URL parameters,
310
     * params from the Symphony cookies. Events and Datasources are executed and
311
     * any parameters  generated by them are appended to the existing parameters
312
     * before setting the Page's XML and XSL variables are set to the be the
313
     * generated XML (from the Datasources and Events) and the XSLT (from the
314
     * file attached to this Page)
315
     *
316
     * @uses FrontendPageResolved
317
     * @uses FrontendParamsResolve
318
     * @uses FrontendParamsPostResolve
319
     * @see resolvePage()
320
     */
321
    private function __buildPage()
0 ignored issues
show
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...
322
    {
323
        $start = precision_timer();
324
325
        if (!$page = $this->resolvePage()) {
326
            throw new FrontendPageNotFoundException;
327
        }
328
329
        /**
330
         * Just after having resolved the page, but prior to any commencement of output creation
331
         * @delegate FrontendPageResolved
332
         * @param string $context
333
         * '/frontend/'
334
         * @param FrontendPage $page
335
         *  An instance of this class, passed by reference
336
         * @param array $page_data
337
         *  An associative array of page data, which is a combination from `tbl_pages` and
338
         *  the path of the page on the filesystem. Passed by reference
339
         */
340
        Symphony::ExtensionManager()->notifyMembers('FrontendPageResolved', '/frontend/', array('page' => &$this, 'page_data' => &$page));
341
342
        $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...
343
        $path = explode('/', $page['path']);
344
        $root_page = is_array($path) ? array_shift($path) : $path;
345
        $current_path = explode(dirname(server_safe('SCRIPT_NAME')), server_safe('REQUEST_URI'), 2);
346
        $current_path = '/' . ltrim(end($current_path), '/');
347
        $split_path = explode('?', $current_path, 3);
348
        $current_path = rtrim(current($split_path), '/');
349
        $querystring = next($split_path);
350
351
        // Get max upload size from php and symphony config then choose the smallest
352
        $upload_size_php = ini_size_to_bytes(ini_get('upload_max_filesize'));
353
        $upload_size_sym = Symphony::Configuration()->get('max_upload_size', 'admin');
354
        $date = new DateTime();
355
356
        $this->_param = array(
357
            'today' => $date->format('Y-m-d'),
358
            'current-time' => $date->format('H:i'),
359
            'this-year' => $date->format('Y'),
360
            'this-month' => $date->format('m'),
361
            'this-day' => $date->format('d'),
362
            'timezone' => $date->format('P'),
363
            'website-name' => Symphony::Configuration()->get('sitename', 'general'),
364
            'page-title' => $page['title'],
365
            'root' => URL,
366
            'workspace' => URL . '/workspace',
367
            'workspace-path' => DIRROOT . '/workspace',
368
            'http-host' => HTTP_HOST,
369
            'root-page' => ($root_page ? $root_page : $page['handle']),
370
            'current-page' => $page['handle'],
371
            'current-page-id' => $page['id'],
372
            'current-path' => ($current_path === '') ? '/' : $current_path,
373
            'parent-path' => '/' . $page['path'],
374
            'current-query-string' => self::sanitizeParameter($querystring),
375
            'current-url' => URL . $current_path,
376
            'upload-limit' => min($upload_size_php, $upload_size_sym),
377
            'symphony-version' => Symphony::Configuration()->get('version', 'symphony'),
378
        );
379
380
        if (isset($this->_env['url']) && is_array($this->_env['url'])) {
381
            foreach ($this->_env['url'] as $key => $val) {
382
                $this->_param[$key] = $val;
383
            }
384
        }
385
386
        if (is_array($_GET) && !empty($_GET)) {
387
            foreach ($_GET as $key => $val) {
388
                if (in_array($key, array('symphony-page', 'debug', 'profile'))) {
389
                    continue;
390
                }
391
392
                // If the browser sends encoded entities for &, ie. a=1&amp;b=2
393
                // this causes the $_GET to output they key as amp;b, which results in
394
                // $url-amp;b. This pattern will remove amp; allow the correct param
395
                // to be used, $url-b
396
                $key = preg_replace('/(^amp;|\/)/', null, $key);
397
398
                // If the key gets replaced out then it will break the XML so prevent
399
                // the parameter being set.
400
                $key = General::createHandle($key);
401
                if (!$key) {
402
                    continue;
403
                }
404
405
                // Handle ?foo[bar]=hi as well as straight ?foo=hi RE: #1348
406
                if (is_array($val)) {
407
                    $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...
408
                } else {
409
                    $val = self::sanitizeParameter($val);
410
                }
411
412
                $this->_param['url-' . $key] = $val;
413
            }
414
        }
415
416
        if (is_array($_COOKIE[__SYM_COOKIE_PREFIX__]) && !empty($_COOKIE[__SYM_COOKIE_PREFIX__])) {
417
            foreach ($_COOKIE[__SYM_COOKIE_PREFIX__] as $key => $val) {
418
                if ($key === 'xsrf-token' && is_array($val)) {
419
                    $val = key($val);
420
                }
421
422
                $this->_param['cookie-' . $key] = $val;
423
            }
424
        }
425
426
        // Flatten parameters:
427
        General::flattenArray($this->_param);
428
429
        // Add Page Types to parameters so they are not flattened too early
430
        $this->_param['page-types'] = $page['type'];
431
432
        // Add Page events the same way
433
        $this->_param['page-events'] = explode(',', trim(str_replace('_', '-', $page['events']), ','));
434
435
        /**
436
         * Just after having resolved the page params, but prior to any commencement of output creation
437
         * @delegate FrontendParamsResolve
438
         * @param string $context
439
         * '/frontend/'
440
         * @param array $params
441
         *  An associative array of this page's parameters
442
         */
443
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsResolve', '/frontend/', array('params' => &$this->_param));
444
445
        $xml_build_start = precision_timer();
446
447
        $xml = new XMLElement('data');
448
        $xml->setIncludeHeader(true);
449
450
        $events = new XMLElement('events');
451
        $this->processEvents($page['events'], $events);
452
        $xml->appendChild($events);
453
454
        $this->_events_xml = clone $events;
455
456
        $this->processDatasources($page['data_sources'], $xml);
457
458
        Symphony::Profiler()->seed($xml_build_start);
459
        Symphony::Profiler()->sample('XML Built', PROFILE_LAP);
460
461
        if (isset($this->_env['pool']) && is_array($this->_env['pool']) && !empty($this->_env['pool'])) {
462
            foreach ($this->_env['pool'] as $handle => $p) {
463
                if (!is_array($p)) {
464
                    $p = array($p);
465
                }
466
467
                foreach ($p as $key => $value) {
468
                    if (is_array($value) && !empty($value)) {
469
                        foreach ($value as $kk => $vv) {
470
                            $this->_param[$handle] .= @implode(', ', $vv) . ',';
471
                        }
472
                    } else {
473
                        $this->_param[$handle] = @implode(', ', $p);
474
                    }
475
                }
476
477
                $this->_param[$handle] = trim($this->_param[$handle], ',');
478
            }
479
        }
480
481
        /**
482
         * Access to the resolved param pool, including additional parameters provided by Data Source outputs
483
         * @delegate FrontendParamsPostResolve
484
         * @param string $context
485
         * '/frontend/'
486
         * @param array $params
487
         *  An associative array of this page's parameters
488
         */
489
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsPostResolve', '/frontend/', array('params' => &$this->_param));
490
491
        $params = new XMLElement('params');
492
        foreach ($this->_param as $key => $value) {
493
            // To support multiple parameters using the 'datasource.field'
494
            // we will pop off the field handle prior to sanitizing the
495
            // key. This is because of a limitation where General::createHandle
496
            // will strip '.' as it's technically punctuation.
497
            if (strpos($key, '.') !== false) {
498
                $parts = explode('.', $key);
499
                $field_handle = '.' . array_pop($parts);
500
                $key = implode('', $parts);
501
            } else {
502
                $field_handle = '';
503
            }
504
505
            $key = Lang::createHandle($key) . $field_handle;
506
            $param = new XMLElement($key);
507
508
            // DS output params get flattened to a string, so get the original pre-flattened array
509
            if (isset($this->_env['pool'][$key])) {
510
                $value = $this->_env['pool'][$key];
511
            }
512
513
            if (is_array($value) && !(count($value) === 1 && empty($value[0]))) {
514
                foreach ($value as $val) {
515
                    $item = new XMLElement('item', General::sanitize($val));
516
                    $param->appendChild($item);
517
                }
518
            } elseif (is_array($value)) {
519
                $param->setValue(General::sanitize($value[0]));
520
            } elseif (in_array($key, array('xsrf-token', 'current-query-string'))) {
521
                $param->setValue(General::wrapInCDATA($value));
522
            } else {
523
                $param->setValue(General::sanitize($value));
524
            }
525
526
            $params->appendChild($param);
527
        }
528
        $xml->prependChild($params);
529
530
        $this->setXML($xml);
531
        $xsl = '<?xml version="1.0" encoding="UTF-8"?>' .
532
               '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' .
533
               '    <xsl:import href="/' . rawurlencode(ltrim($page['filelocation'], '/')) . '"/>' .
534
               '</xsl:stylesheet>';
535
536
        $this->setXSL($xsl, false);
537
        $this->setRuntimeParam($this->_param);
538
539
        Symphony::Profiler()->seed($start);
540
        Symphony::Profiler()->sample('Page Built', PROFILE_LAP);
541
    }
542
543
    /**
544
     * This function attempts to resolve the given page in to it's Symphony page. If no
545
     * page is given, it is assumed the 'index' is being requested. Before a page row is
546
     * returned, it is checked to see that if it has the 'admin' type, that the requesting
547
     * user is authenticated as a Symphony author. If they are not, the Symphony 403
548
     * page is returned (whether that be set as a user defined page using the page type
549
     * of 403, or just returning the Default Symphony 403 error page). Any URL parameters
550
     * set on the page are added to the `$env` variable before the function returns an
551
     * associative array of page details such as Title, Content Type etc.
552
     *
553
     * @uses FrontendPrePageResolve
554
     * @see __isSchemaValid()
555
     * @param string $page
556
     * The URL of the current page that is being Rendered as returned by `getCurrentPage()`.
557
     * If no URL is provided, Symphony assumes the Page with the type 'index' is being
558
     * requested.
559
     * @throws SymphonyErrorPage
560
     * @return array
561
     *  An associative array of page details
562
     */
563
    public function resolvePage($page = null)
564
    {
565
        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...
566
            $this->_page = $page;
567
        }
568
569
        $row = null;
570
        /**
571
         * Before page resolve. Allows manipulation of page without redirection
572
         * @delegate FrontendPrePageResolve
573
         * @param string $context
574
         * '/frontend/'
575
         * @param mixed $row
576
         * @param FrontendPage $page
577
         *  An instance of this FrontendPage
578
         */
579
        Symphony::ExtensionManager()->notifyMembers('FrontendPrePageResolve', '/frontend/', array('row' => &$row, 'page' => &$this->_page));
580
581
        // Default to the index page if no page has been specified
582
        if ((!$this->_page || $this->_page === '//') && is_null($row)) {
583
            $row = PageManager::fetchPageByType('index');
584
585
            // Not the index page (or at least not on first impression)
586
        } elseif (is_null($row)) {
587
            $page_extra_bits = array();
588
            $pathArr = preg_split('/\//', trim($this->_page, '/'), -1, PREG_SPLIT_NO_EMPTY);
589
            $handle = array_pop($pathArr);
590
591
            do {
592
                $path = implode('/', $pathArr);
593
594
                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...
595
                    $pathArr[] = $handle;
596
597
                    break 1;
598
                } else {
599
                    $page_extra_bits[] = $handle;
600
                }
601
            } while (($handle = array_pop($pathArr)) !== null);
602
603
            // If the `$pathArr` is empty, that means a page hasn't resolved for
604
            // the given `$page`, however in some cases the index page may allow
605
            // parameters, so we'll check.
606
            if (empty($pathArr)) {
607
                // If the index page does not handle parameters, then return false
608
                // (which will give up the 404), otherwise treat the `$page` as
609
                // parameters of the index. RE: #1351
610
                $index = PageManager::fetchPageByType('index');
611
612
                if (!$this->__isSchemaValid($index['params'], $page_extra_bits)) {
613
                    return false;
614
                } else {
615
                    $row = $index;
616
                }
617
618
                // Page resolved, check the schema (are the parameters valid?)
619
            } elseif (!$this->__isSchemaValid($row['params'], $page_extra_bits)) {
620
                return false;
621
            }
622
        }
623
624
        // Nothing resolved, bail now
625
        if (!is_array($row) || empty($row)) {
626
            return false;
627
        }
628
629
        // Process the extra URL params
630
        $url_params = preg_split('/\//', $row['params'], -1, PREG_SPLIT_NO_EMPTY);
631
632
        foreach ($url_params as $var) {
633
            $this->_env['url'][$var] = null;
634
        }
635
636
        if (isset($page_extra_bits)) {
637
            if (!empty($page_extra_bits)) {
638
                $page_extra_bits = array_reverse($page_extra_bits);
639
            }
640
641
            for ($i = 0, $ii = count($page_extra_bits); $i < $ii; $i++) {
642
                $this->_env['url'][$url_params[$i]] = str_replace(' ', '+', $page_extra_bits[$i]);
643
            }
644
        }
645
646
        $row['type'] = PageManager::fetchPageTypes($row['id']);
647
648
        // Make sure the user has permission to access this page
649
        if (!$this->is_logged_in && in_array('admin', $row['type'])) {
650
            $row = PageManager::fetchPageByType('403');
651
652
            if (empty($row)) {
653
                Frontend::instance()->throwCustomError(
654
                    __('Please login to view this page.') . ' <a href="' . SYMPHONY_URL . '/login/">' . __('Take me to the login page') . '</a>.',
655
                    __('Forbidden'),
656
                    Page::HTTP_STATUS_FORBIDDEN
657
                );
658
            }
659
660
            $row['type'] = PageManager::fetchPageTypes($row['id']);
661
        }
662
663
        $row['filelocation'] = PageManager::resolvePageFileLocation($row['path'], $row['handle']);
664
665
        return $row;
666
    }
667
668
    /**
669
     * Given the allowed params for the resolved page, compare it to
670
     * params provided in the URL. If the number of params provided
671
     * is less than or equal to the number of URL parameters set for a page,
672
     * the Schema is considered valid, otherwise, it's considered to be false
673
     * a 404 page will result.
674
     *
675
     * @param string $schema
676
     *  The URL schema for a page, ie. article/read
677
     * @param array $bits
678
     *  The URL parameters provided from parsing the current URL. This
679
     *  does not include any `$_GET` or `$_POST` variables.
680
     * @return boolean
681
     *  True if the number of $schema (split by /) is less than the size
682
     *  of the $bits array.
683
     */
684
    private function __isSchemaValid($schema, array $bits)
685
    {
686
        $schema_arr = preg_split('/\//', $schema, -1, PREG_SPLIT_NO_EMPTY);
687
688
        return (count($schema_arr) >= count($bits));
689
    }
690
691
    /**
692
     * The processEvents function executes all Events attached to the resolved
693
     * page in the correct order determined by `__findEventOrder()`. The results
694
     * from the Events are appended to the page's XML. Events execute first,
695
     * before Datasources.
696
     *
697
     * @uses FrontendProcessEvents
698
     * @uses FrontendEventPostProcess
699
     * @param string $events
700
     *  A string of all the Events attached to this page, comma separated.
701
     * @param XMLElement $wrapper
702
     *  The XMLElement to append the Events results to. Event results are
703
     *  contained in a root XMLElement that is the handlised version of
704
     *  their name.
705
     * @throws Exception
706
     */
707
    private function processEvents($events, XMLElement &$wrapper)
708
    {
709
        /**
710
         * Manipulate the events array and event element wrapper
711
         * @delegate FrontendProcessEvents
712
         * @param string $context
713
         * '/frontend/'
714
         * @param array $env
715
         * @param string $events
716
         *  A string of all the Events attached to this page, comma separated.
717
         * @param XMLElement $wrapper
718
         *  The XMLElement to append the Events results to. Event results are
719
         *  contained in a root XMLElement that is the handlised version of
720
         *  their name.
721
         * @param array $page_data
722
         *  An associative array of page meta data
723
         */
724
        Symphony::ExtensionManager()->notifyMembers('FrontendProcessEvents', '/frontend/', array(
725
            'env' => $this->_env,
726
            'events' => &$events,
727
            'wrapper' => &$wrapper,
728
            'page_data' => $this->_pageData
729
        ));
730
731
        if (strlen(trim($events)) > 0) {
732
            $events = preg_split('/,\s*/i', $events, -1, PREG_SPLIT_NO_EMPTY);
733
            $events = array_map('trim', $events);
734
735
            if (!is_array($events) || empty($events)) {
736
                return;
737
            }
738
739
            $pool = array();
740
741
            foreach ($events as $handle) {
742
                $pool[$handle] = EventManager::create($handle, array('env' => $this->_env, 'param' => $this->_param));
743
            }
744
745
            uasort($pool, array($this, '__findEventOrder'));
746
747
            foreach ($pool as $handle => $event) {
748
                $startTime = precision_timer();
749
                $queries = Symphony::Database()->queryCount();
750
751
                if ($xml = $event->load()) {
752
                    if (is_object($xml)) {
753
                        $wrapper->appendChild($xml);
754
                    } else {
755
                        $wrapper->setValue(
756
                            $wrapper->getValue() . PHP_EOL . '    ' . trim($xml)
757
                        );
758
                    }
759
                }
760
761
                $queries = Symphony::Database()->queryCount() - $queries;
762
                Symphony::Profiler()->seed($startTime);
763
                Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Event', $queries);
764
            }
765
        }
766
767
        /**
768
         * Just after the page events have triggered. Provided with the XML object
769
         * @delegate FrontendEventPostProcess
770
         * @param string $context
771
         * '/frontend/'
772
         * @param XMLElement $xml
773
         *  The XMLElement to append the Events results to. Event results are
774
         *  contained in a root XMLElement that is the handlised version of
775
         *  their name.
776
         */
777
        Symphony::ExtensionManager()->notifyMembers('FrontendEventPostProcess', '/frontend/', array('xml' => &$wrapper));
778
    }
779
780
    /**
781
     * This function determines the correct order that events should be executed in.
782
     * Events are executed based off priority, with `Event::kHIGH` priority executing
783
     * first. If there is more than one Event of the same priority, they are then
784
     * executed in alphabetical order. This function is designed to be used with
785
     * PHP's uasort function.
786
     *
787
     * @link http://php.net/manual/en/function.uasort.php
788
     * @param Event $a
789
     * @param Event $b
790
     * @return integer
791
     */
792
    private function __findEventOrder($a, $b)
793
    {
794
        if ($a->priority() === $b->priority()) {
795
            $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...
796
            $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...
797
798
            $handles = array($a['name'], $b['name']);
799
            asort($handles);
800
801
            return (key($handles) === 0) ? -1 : 1;
802
        }
803
        return(($a->priority() > $b->priority()) ? -1 : 1);
804
    }
805
806
    /**
807
     * Given an array of all the Datasources for this page, sort them into the
808
     * correct execution order and append the Datasource results to the
809
     * page XML. If the Datasource provides any parameters, they will be
810
     * added to the `$env` pool for use by other Datasources and eventual
811
     * inclusion into the page parameters.
812
     *
813
     * @param string $datasources
814
     *  A string of Datasource's attached to this page, comma separated.
815
     * @param XMLElement $wrapper
816
     *  The XMLElement to append the Datasource results to. Datasource
817
     *  results are contained in a root XMLElement that is the handlised
818
     *  version of their name.
819
     * @param array $params
820
     *  Any params to automatically add to the `$env` pool, by default this
821
     *  is an empty array. It looks like Symphony does not utilise this parameter
822
     *  at all
823
     * @throws Exception
824
     */
825
    public function processDatasources($datasources, XMLElement &$wrapper, array $params = array())
826
    {
827
        if (trim($datasources) === '') {
828
            return;
829
        }
830
831
        $datasources = preg_split('/,\s*/i', $datasources, -1, PREG_SPLIT_NO_EMPTY);
832
        $datasources = array_map('trim', $datasources);
833
834
        if (!is_array($datasources) || empty($datasources)) {
835
            return;
836
        }
837
838
        $this->_env['pool'] = $params;
839
        $pool = $params;
840
        $dependencies = array();
841
842
        foreach ($datasources as $handle) {
843
            $pool[$handle] = DatasourceManager::create($handle, array(), false);
844
            $dependencies[$handle] = $pool[$handle]->getDependencies();
845
        }
846
847
        $dsOrder = $this->__findDatasourceOrder($dependencies);
848
849
        foreach ($dsOrder as $handle) {
850
            $startTime = precision_timer();
851
            $queries = Symphony::Database()->queryCount();
852
853
            // default to no XML
854
            $xml = null;
855
            $ds = $pool[$handle];
856
857
            // Handle redirect on empty setting correctly RE: #1539
858
            try {
859
                $ds->processParameters(array('env' => $this->_env, 'param' => $this->_param));
860
            } catch (FrontendPageNotFoundException $e) {
861
                // Work around. This ensures the 404 page is displayed and
862
                // is not picked up by the default catch() statement below
863
                FrontendPageNotFoundExceptionHandler::render($e);
864
            }
865
866
            /**
867
             * Allows extensions to execute the data source themselves (e.g. for caching)
868
             * and providing their own output XML instead
869
             *
870
             * @since Symphony 2.3
871
             * @delegate DataSourcePreExecute
872
             * @param string $context
873
             * '/frontend/'
874
             * @param DataSource $datasource
875
             *  The Datasource object
876
             * @param mixed $xml
877
             *  The XML output of the data source. Can be an `XMLElement` or string.
878
             * @param array $param_pool
879
             *  The existing param pool including output parameters of any previous data sources
880
             */
881
            Symphony::ExtensionManager()->notifyMembers('DataSourcePreExecute', '/frontend/', array(
882
                'datasource' => &$ds,
883
                'xml' => &$xml,
884
                'param_pool' => &$this->_env['pool']
885
            ));
886
887
            // if the XML is still null, an extension has not run the data source, so run normally
888
            if (is_null($xml)) {
889
                $xml = $ds->execute($this->_env['pool']);
890
            }
891
892
            if ($xml) {
893
                /**
894
                 * After the datasource has executed, either by itself or via the
895
                 * `DataSourcePreExecute` delegate, and if the `$xml` variable is truthy,
896
                 * this delegate allows extensions to modify the output XML and parameter pool
897
                 *
898
                 * @since Symphony 2.3
899
                 * @delegate DataSourcePostExecute
900
                 * @param string $context
901
                 * '/frontend/'
902
                 * @param DataSource $datasource
903
                 *  The Datasource object
904
                 * @param mixed $xml
905
                 *  The XML output of the data source. Can be an `XMLElement` or string.
906
                 * @param array $param_pool
907
                 *  The existing param pool including output parameters of any previous data sources
908
                 */
909
                Symphony::ExtensionManager()->notifyMembers('DataSourcePostExecute', '/frontend/', array(
910
                    'datasource' => $ds,
911
                    'xml' => &$xml,
912
                    'param_pool' => &$this->_env['pool']
913
                ));
914
915
                if ($xml instanceof XMLElement) {
916
                    $wrapper->appendChild($xml);
917
                } else {
918
                    $wrapper->appendChild(
919
                        '    ' . trim($xml) . PHP_EOL
920
                    );
921
                }
922
            }
923
924
            $queries = Symphony::Database()->queryCount() - $queries;
925
            Symphony::Profiler()->seed($startTime);
926
            Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Datasource', $queries);
927
            unset($ds);
928
        }
929
    }
930
931
    /**
932
     * The function finds the correct order Datasources need to be processed in to
933
     * satisfy all dependencies that parameters can resolve correctly and in time for
934
     * other Datasources to filter on.
935
     *
936
     * @param array $dependenciesList
937
     *  An associative array with the key being the Datasource handle and the values
938
     *  being it's dependencies.
939
     * @return array
940
     *  The sorted array of Datasources in order of how they should be executed
941
     */
942
    private function __findDatasourceOrder($dependenciesList)
943
    {
944
        if (!is_array($dependenciesList) || empty($dependenciesList)) {
945
            return array();
946
        }
947
948
        foreach ($dependenciesList as $handle => $dependencies) {
949
            foreach ($dependencies as $i => $dependency) {
950
                $dependency = explode('.', $dependency);
951
                $dependenciesList[$handle][$i] = reset($dependency);
952
            }
953
        }
954
955
        $orderedList = array();
956
        $dsKeyArray = $this->__buildDatasourcePooledParamList(array_keys($dependenciesList));
957
958
        // 1. First do a cleanup of each dependency list, removing non-existant DS's and find
959
        //    the ones that have no dependencies, removing them from the list
960
        foreach ($dependenciesList as $handle => $dependencies) {
961
            $dependenciesList[$handle] = @array_intersect($dsKeyArray, $dependencies);
962
963
            if (empty($dependenciesList[$handle])) {
964
                unset($dependenciesList[$handle]);
965
                $orderedList[] = str_replace('_', '-', $handle);
966
            }
967
        }
968
969
        // 2. Iterate over the remaining DS's. Find if all their dependencies are
970
        //    in the $orderedList array. Keep iterating until all DS's are in that list
971
        //    or there are circular dependencies (list doesn't change between iterations
972
        //    of the while loop)
973
        do {
974
            $last_count = count($dependenciesList);
975
976
            foreach ($dependenciesList as $handle => $dependencies) {
977
                if (General::in_array_all(array_map(function($a) {return str_replace('$ds-', '', $a);}, $dependencies), $orderedList)) {
978
                    $orderedList[] = str_replace('_', '-', $handle);
979
                    unset($dependenciesList[$handle]);
980
                }
981
            }
982
        } while (!empty($dependenciesList) && $last_count > count($dependenciesList));
983
984
        if (!empty($dependenciesList)) {
985
            $orderedList = array_merge($orderedList, array_keys($dependenciesList));
986
        }
987
988
        return array_map(function($a) {return str_replace('-', '_', $a);}, $orderedList);
989
    }
990
991
    /**
992
     * Given an array of datasource dependancies, this function will translate
993
     * each of them to be a valid datasource handle.
994
     *
995
     * @param array $datasources
996
     *  The datasource dependencies
997
     * @return array
998
     *  An array of the handlised datasources
999
     */
1000
    private function __buildDatasourcePooledParamList($datasources)
1001
    {
1002
        if (!is_array($datasources) || empty($datasources)) {
1003
            return array();
1004
        }
1005
1006
        $list = array();
1007
1008
        foreach ($datasources as $handle) {
1009
            $rootelement = str_replace('_', '-', $handle);
1010
            $list[] = '$ds-' . $rootelement;
1011
        }
1012
1013
        return $list;
1014
    }
1015
1016
    /**
1017
     * Given a string (expected to be a URL parameter) this function will
1018
     * ensure it is safe to embed in an XML document.
1019
     *
1020
     * @since Symphony 2.3.1
1021
     * @param string $parameter
1022
     *  The string to sanitize for XML
1023
     * @return string
1024
     *  The sanitized string
1025
     */
1026
    public static function sanitizeParameter($parameter)
1027
    {
1028
        return XMLElement::stripInvalidXMLCharacters($parameter);
1029
    }
1030
}
1031