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 ( 5cdb31...81c93f )
by Brendan
04:03
created

FrontendPage::__buildPage()   F

Complexity

Conditions 37
Paths 2177

Size

Total Lines 220
Code Lines 117

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 37
eloc 117
c 3
b 2
f 0
nc 2177
nop 0
dl 0
loc 220
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 $_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...
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['SCRIPT_NAME']), $_SERVER['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
            'http-host' => HTTP_HOST,
368
            'root-page' => ($root_page ? $root_page : $page['handle']),
369
            'current-page' => $page['handle'],
370
            'current-page-id' => $page['id'],
371
            'current-path' => ($current_path === '') ? '/' : $current_path,
372
            'parent-path' => '/' . $page['path'],
373
            'current-query-string' => self::sanitizeParameter($querystring),
374
            'current-url' => URL . $current_path,
375
            'upload-limit' => min($upload_size_php, $upload_size_sym),
376
            'symphony-version' => Symphony::Configuration()->get('version', 'symphony'),
377
        );
378
379
        if (isset($this->_env['url']) && is_array($this->_env['url'])) {
380
            foreach ($this->_env['url'] as $key => $val) {
381
                $this->_param[$key] = $val;
382
            }
383
        }
384
385
        if (is_array($_GET) && !empty($_GET)) {
386
            foreach ($_GET as $key => $val) {
387
                if (in_array($key, array('symphony-page', 'debug', 'profile'))) {
388
                    continue;
389
                }
390
391
                // If the browser sends encoded entities for &, ie. a=1&amp;b=2
392
                // this causes the $_GET to output they key as amp;b, which results in
393
                // $url-amp;b. This pattern will remove amp; allow the correct param
394
                // to be used, $url-b
395
                $key = preg_replace('/(^amp;|\/)/', null, $key);
396
397
                // If the key gets replaced out then it will break the XML so prevent
398
                // the parameter being set.
399
                $key = General::createHandle($key);
400
                if (!$key) {
401
                    continue;
402
                }
403
404
                // Handle ?foo[bar]=hi as well as straight ?foo=hi RE: #1348
405
                if (is_array($val)) {
406
                    $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...
407
                } else {
408
                    $val = self::sanitizeParameter($val);
409
                }
410
411
                $this->_param['url-' . $key] = $val;
412
            }
413
        }
414
415
        if (is_array($_COOKIE[__SYM_COOKIE_PREFIX__]) && !empty($_COOKIE[__SYM_COOKIE_PREFIX__])) {
416
            foreach ($_COOKIE[__SYM_COOKIE_PREFIX__] as $key => $val) {
417
                if ($key === 'xsrf-token' && is_array($val)) {
418
                    $val = key($val);
419
                }
420
421
                $this->_param['cookie-' . $key] = $val;
422
            }
423
        }
424
425
        // Flatten parameters:
426
        General::flattenArray($this->_param);
427
428
        // Add Page Types to parameters so they are not flattened too early
429
        $this->_param['page-types'] = $page['type'];
430
431
        // Add Page events the same way
432
        $this->_param['page-events'] = explode(',', trim(str_replace('_', '-', $page['events']), ','));
433
434
        /**
435
         * Just after having resolved the page params, but prior to any commencement of output creation
436
         * @delegate FrontendParamsResolve
437
         * @param string $context
438
         * '/frontend/'
439
         * @param array $params
440
         *  An associative array of this page's parameters
441
         */
442
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsResolve', '/frontend/', array('params' => &$this->_param));
443
444
        $xml_build_start = precision_timer();
445
446
        $xml = new XMLElement('data');
447
        $xml->setIncludeHeader(true);
448
449
        $events = new XMLElement('events');
450
        $this->processEvents($page['events'], $events);
451
        $xml->appendChild($events);
452
453
        $this->_events_xml = clone $events;
454
455
        $this->processDatasources($page['data_sources'], $xml);
456
457
        Symphony::Profiler()->seed($xml_build_start);
458
        Symphony::Profiler()->sample('XML Built', PROFILE_LAP);
459
460
        if (isset($this->_env['pool']) && is_array($this->_env['pool']) && !empty($this->_env['pool'])) {
461
            foreach ($this->_env['pool'] as $handle => $p) {
462
                if (!is_array($p)) {
463
                    $p = array($p);
464
                }
465
466
                foreach ($p as $key => $value) {
467
                    if (is_array($value) && !empty($value)) {
468
                        foreach ($value as $kk => $vv) {
469
                            $this->_param[$handle] .= @implode(', ', $vv) . ',';
470
                        }
471
                    } else {
472
                        $this->_param[$handle] = @implode(', ', $p);
473
                    }
474
                }
475
476
                $this->_param[$handle] = trim($this->_param[$handle], ',');
477
            }
478
        }
479
480
        /**
481
         * Access to the resolved param pool, including additional parameters provided by Data Source outputs
482
         * @delegate FrontendParamsPostResolve
483
         * @param string $context
484
         * '/frontend/'
485
         * @param array $params
486
         *  An associative array of this page's parameters
487
         */
488
        Symphony::ExtensionManager()->notifyMembers('FrontendParamsPostResolve', '/frontend/', array('params' => &$this->_param));
489
490
        $params = new XMLElement('params');
491
        foreach ($this->_param as $key => $value) {
492
            // To support multiple parameters using the 'datasource.field'
493
            // we will pop off the field handle prior to sanitizing the
494
            // key. This is because of a limitation where General::createHandle
495
            // will strip '.' as it's technically punctuation.
496
            if (strpos($key, '.') !== false) {
497
                $parts = explode('.', $key);
498
                $field_handle = '.' . array_pop($parts);
499
                $key = implode('', $parts);
500
            } else {
501
                $field_handle = '';
502
            }
503
504
            $key = Lang::createHandle($key) . $field_handle;
505
            $param = new XMLElement($key);
506
507
            // DS output params get flattened to a string, so get the original pre-flattened array
508
            if (isset($this->_env['pool'][$key])) {
509
                $value = $this->_env['pool'][$key];
510
            }
511
512
            if (is_array($value) && !(count($value) === 1 && empty($value[0]))) {
513
                foreach ($value as $val) {
514
                    $item = new XMLElement('item', General::sanitize($val));
515
                    $param->appendChild($item);
516
                }
517
            } elseif (is_array($value)) {
518
                $param->setValue(General::sanitize($value[0]));
519
            } elseif (in_array($key, array('xsrf-token', 'current-query-string'))) {
520
                $param->setValue(General::wrapInCDATA($value));
521
            } else {
522
                $param->setValue(General::sanitize($value));
523
            }
524
525
            $params->appendChild($param);
526
        }
527
        $xml->prependChild($params);
528
529
        $this->setXML($xml);
530
        $xsl = '<?xml version="1.0" encoding="UTF-8"?>' .
531
               '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' .
532
               '    <xsl:import href="/' . rawurlencode(ltrim($page['filelocation'], '/')) . '"/>' .
533
               '</xsl:stylesheet>';
534
535
        $this->setXSL($xsl, false);
536
        $this->setRuntimeParam($this->_param);
537
538
        Symphony::Profiler()->seed($start);
539
        Symphony::Profiler()->sample('Page Built', PROFILE_LAP);
540
    }
541
542
    /**
543
     * This function attempts to resolve the given page in to it's Symphony page. If no
544
     * page is given, it is assumed the 'index' is being requested. Before a page row is
545
     * returned, it is checked to see that if it has the 'admin' type, that the requesting
546
     * user is authenticated as a Symphony author. If they are not, the Symphony 403
547
     * page is returned (whether that be set as a user defined page using the page type
548
     * of 403, or just returning the Default Symphony 403 error page). Any URL parameters
549
     * set on the page are added to the `$env` variable before the function returns an
550
     * associative array of page details such as Title, Content Type etc.
551
     *
552
     * @uses FrontendPrePageResolve
553
     * @see __isSchemaValid()
554
     * @param string $page
555
     * The URL of the current page that is being Rendered as returned by `getCurrentPage()`.
556
     * If no URL is provided, Symphony assumes the Page with the type 'index' is being
557
     * requested.
558
     * @throws SymphonyErrorPage
559
     * @return array
560
     *  An associative array of page details
561
     */
562
    public function resolvePage($page = null)
563
    {
564
        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...
565
            $this->_page = $page;
566
        }
567
568
        $row = null;
569
        /**
570
         * Before page resolve. Allows manipulation of page without redirection
571
         * @delegate FrontendPrePageResolve
572
         * @param string $context
573
         * '/frontend/'
574
         * @param mixed $row
575
         * @param FrontendPage $page
576
         *  An instance of this FrontendPage
577
         */
578
        Symphony::ExtensionManager()->notifyMembers('FrontendPrePageResolve', '/frontend/', array('row' => &$row, 'page' => &$this->_page));
579
580
        // Default to the index page if no page has been specified
581
        if ((!$this->_page || $this->_page === '//') && is_null($row)) {
582
            $row = PageManager::fetchPageByType('index');
583
584
            // Not the index page (or at least not on first impression)
585
        } elseif (is_null($row)) {
586
            $page_extra_bits = array();
587
            $pathArr = preg_split('/\//', trim($this->_page, '/'), -1, PREG_SPLIT_NO_EMPTY);
588
            $handle = array_pop($pathArr);
589
590
            do {
591
                $path = implode('/', $pathArr);
592
593
                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...
594
                    $pathArr[] = $handle;
595
596
                    break 1;
597
                } else {
598
                    $page_extra_bits[] = $handle;
599
                }
600
            } while (($handle = array_pop($pathArr)) !== null);
601
602
            // If the `$pathArr` is empty, that means a page hasn't resolved for
603
            // the given `$page`, however in some cases the index page may allow
604
            // parameters, so we'll check.
605
            if (empty($pathArr)) {
606
                // If the index page does not handle parameters, then return false
607
                // (which will give up the 404), otherwise treat the `$page` as
608
                // parameters of the index. RE: #1351
609
                $index = PageManager::fetchPageByType('index');
610
611
                if (!$this->__isSchemaValid($index['params'], $page_extra_bits)) {
612
                    return false;
613
                } else {
614
                    $row = $index;
615
                }
616
617
                // Page resolved, check the schema (are the parameters valid?)
618
            } elseif (!$this->__isSchemaValid($row['params'], $page_extra_bits)) {
619
                return false;
620
            }
621
        }
622
623
        // Nothing resolved, bail now
624
        if (!is_array($row) || empty($row)) {
625
            return false;
626
        }
627
628
        // Process the extra URL params
629
        $url_params = preg_split('/\//', $row['params'], -1, PREG_SPLIT_NO_EMPTY);
630
631
        foreach ($url_params as $var) {
632
            $this->_env['url'][$var] = null;
633
        }
634
635
        if (isset($page_extra_bits)) {
636
            if (!empty($page_extra_bits)) {
637
                $page_extra_bits = array_reverse($page_extra_bits);
638
            }
639
640
            for ($i = 0, $ii = count($page_extra_bits); $i < $ii; $i++) {
641
                $this->_env['url'][$url_params[$i]] = str_replace(' ', '+', $page_extra_bits[$i]);
642
            }
643
        }
644
645
        $row['type'] = PageManager::fetchPageTypes($row['id']);
646
647
        // Make sure the user has permission to access this page
648
        if (!$this->is_logged_in && in_array('admin', $row['type'])) {
649
            $row = PageManager::fetchPageByType('403');
650
651
            if (empty($row)) {
652
                Frontend::instance()->throwCustomError(
653
                    __('Please login to view this page.') . ' <a href="' . SYMPHONY_URL . '/login/">' . __('Take me to the login page') . '</a>.',
654
                    __('Forbidden'),
655
                    Page::HTTP_STATUS_FORBIDDEN
656
                );
657
            }
658
659
            $row['type'] = PageManager::fetchPageTypes($row['id']);
660
        }
661
662
        $row['filelocation'] = PageManager::resolvePageFileLocation($row['path'], $row['handle']);
663
664
        return $row;
665
    }
666
667
    /**
668
     * Given the allowed params for the resolved page, compare it to
669
     * params provided in the URL. If the number of params provided
670
     * is less than or equal to the number of URL parameters set for a page,
671
     * the Schema is considered valid, otherwise, it's considered to be false
672
     * a 404 page will result.
673
     *
674
     * @param string $schema
675
     *  The URL schema for a page, ie. article/read
676
     * @param array $bits
677
     *  The URL parameters provided from parsing the current URL. This
678
     *  does not include any `$_GET` or `$_POST` variables.
679
     * @return boolean
680
     *  True if the number of $schema (split by /) is less than the size
681
     *  of the $bits array.
682
     */
683
    private function __isSchemaValid($schema, array $bits)
684
    {
685
        $schema_arr = preg_split('/\//', $schema, -1, PREG_SPLIT_NO_EMPTY);
686
687
        return (count($schema_arr) >= count($bits));
688
    }
689
690
    /**
691
     * The processEvents function executes all Events attached to the resolved
692
     * page in the correct order determined by `__findEventOrder()`. The results
693
     * from the Events are appended to the page's XML. Events execute first,
694
     * before Datasources.
695
     *
696
     * @uses FrontendProcessEvents
697
     * @uses FrontendEventPostProcess
698
     * @param string $events
699
     *  A string of all the Events attached to this page, comma separated.
700
     * @param XMLElement $wrapper
701
     *  The XMLElement to append the Events results to. Event results are
702
     *  contained in a root XMLElement that is the handlised version of
703
     *  their name.
704
     * @throws Exception
705
     */
706
    private function processEvents($events, XMLElement &$wrapper)
707
    {
708
        /**
709
         * Manipulate the events array and event element wrapper
710
         * @delegate FrontendProcessEvents
711
         * @param string $context
712
         * '/frontend/'
713
         * @param array $env
714
         * @param string $events
715
         *  A string of all the Events attached to this page, comma separated.
716
         * @param XMLElement $wrapper
717
         *  The XMLElement to append the Events results to. Event results are
718
         *  contained in a root XMLElement that is the handlised version of
719
         *  their name.
720
         * @param array $page_data
721
         *  An associative array of page meta data
722
         */
723
        Symphony::ExtensionManager()->notifyMembers('FrontendProcessEvents', '/frontend/', array(
724
            'env' => $this->_env,
725
            'events' => &$events,
726
            'wrapper' => &$wrapper,
727
            'page_data' => $this->_pageData
728
        ));
729
730
        if (strlen(trim($events)) > 0) {
731
            $events = preg_split('/,\s*/i', $events, -1, PREG_SPLIT_NO_EMPTY);
732
            $events = array_map('trim', $events);
733
734
            if (!is_array($events) || empty($events)) {
735
                return;
736
            }
737
738
            $pool = array();
739
740
            foreach ($events as $handle) {
741
                $pool[$handle] = EventManager::create($handle, array('env' => $this->_env, 'param' => $this->_param));
742
            }
743
744
            uasort($pool, array($this, '__findEventOrder'));
745
746
            foreach ($pool as $handle => $event) {
747
                $startTime = precision_timer();
748
                $queries = Symphony::Database()->queryCount();
749
750
                if ($xml = $event->load()) {
751
                    if (is_object($xml)) {
752
                        $wrapper->appendChild($xml);
753
                    } else {
754
                        $wrapper->setValue(
755
                            $wrapper->getValue() . PHP_EOL . '    ' . trim($xml)
756
                        );
757
                    }
758
                }
759
760
                $queries = Symphony::Database()->queryCount() - $queries;
761
                Symphony::Profiler()->seed($startTime);
762
                Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Event', $queries);
763
            }
764
        }
765
766
        /**
767
         * Just after the page events have triggered. Provided with the XML object
768
         * @delegate FrontendEventPostProcess
769
         * @param string $context
770
         * '/frontend/'
771
         * @param XMLElement $xml
772
         *  The XMLElement to append the Events results to. Event results are
773
         *  contained in a root XMLElement that is the handlised version of
774
         *  their name.
775
         */
776
        Symphony::ExtensionManager()->notifyMembers('FrontendEventPostProcess', '/frontend/', array('xml' => &$wrapper));
777
    }
778
779
    /**
780
     * This function determines the correct order that events should be executed in.
781
     * Events are executed based off priority, with `Event::kHIGH` priority executing
782
     * first. If there is more than one Event of the same priority, they are then
783
     * executed in alphabetical order. This function is designed to be used with
784
     * PHP's uasort function.
785
     *
786
     * @link http://php.net/manual/en/function.uasort.php
787
     * @param Event $a
788
     * @param Event $b
789
     * @return integer
790
     */
791
    private function __findEventOrder($a, $b)
792
    {
793
        if ($a->priority() === $b->priority()) {
794
            $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...
795
            $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...
796
797
            $handles = array($a['name'], $b['name']);
798
            asort($handles);
799
800
            return (key($handles) === 0) ? -1 : 1;
801
        }
802
        return(($a->priority() > $b->priority()) ? -1 : 1);
803
    }
804
805
    /**
806
     * Given an array of all the Datasources for this page, sort them into the
807
     * correct execution order and append the Datasource results to the
808
     * page XML. If the Datasource provides any parameters, they will be
809
     * added to the `$env` pool for use by other Datasources and eventual
810
     * inclusion into the page parameters.
811
     *
812
     * @param string $datasources
813
     *  A string of Datasource's attached to this page, comma separated.
814
     * @param XMLElement $wrapper
815
     *  The XMLElement to append the Datasource results to. Datasource
816
     *  results are contained in a root XMLElement that is the handlised
817
     *  version of their name.
818
     * @param array $params
819
     *  Any params to automatically add to the `$env` pool, by default this
820
     *  is an empty array. It looks like Symphony does not utilise this parameter
821
     *  at all
822
     * @throws Exception
823
     */
824
    public function processDatasources($datasources, XMLElement &$wrapper, array $params = array())
825
    {
826
        if (trim($datasources) === '') {
827
            return;
828
        }
829
830
        $datasources = preg_split('/,\s*/i', $datasources, -1, PREG_SPLIT_NO_EMPTY);
831
        $datasources = array_map('trim', $datasources);
832
833
        if (!is_array($datasources) || empty($datasources)) {
834
            return;
835
        }
836
837
        $this->_env['pool'] = $params;
838
        $pool = $params;
839
        $dependencies = array();
840
841
        foreach ($datasources as $handle) {
842
            $pool[$handle] = DatasourceManager::create($handle, array(), false);
843
            $dependencies[$handle] = $pool[$handle]->getDependencies();
844
        }
845
846
        $dsOrder = $this->__findDatasourceOrder($dependencies);
847
848
        foreach ($dsOrder as $handle) {
849
            $startTime = precision_timer();
850
            $queries = Symphony::Database()->queryCount();
851
852
            // default to no XML
853
            $xml = null;
854
            $ds = $pool[$handle];
855
856
            // Handle redirect on empty setting correctly RE: #1539
857
            try {
858
                $ds->processParameters(array('env' => $this->_env, 'param' => $this->_param));
859
            } catch (FrontendPageNotFoundException $e) {
860
                // Work around. This ensures the 404 page is displayed and
861
                // is not picked up by the default catch() statement below
862
                FrontendPageNotFoundExceptionHandler::render($e);
863
            }
864
865
            /**
866
             * Allows extensions to execute the data source themselves (e.g. for caching)
867
             * and providing their own output XML instead
868
             *
869
             * @since Symphony 2.3
870
             * @delegate DataSourcePreExecute
871
             * @param string $context
872
             * '/frontend/'
873
             * @param DataSource $datasource
874
             *  The Datasource object
875
             * @param mixed $xml
876
             *  The XML output of the data source. Can be an `XMLElement` or string.
877
             * @param array $param_pool
878
             *  The existing param pool including output parameters of any previous data sources
879
             */
880
            Symphony::ExtensionManager()->notifyMembers('DataSourcePreExecute', '/frontend/', array(
881
                'datasource' => &$ds,
882
                'xml' => &$xml,
883
                'param_pool' => &$this->_env['pool']
884
            ));
885
886
            // if the XML is still null, an extension has not run the data source, so run normally
887
            if (is_null($xml)) {
888
                $xml = $ds->execute($this->_env['pool']);
889
            }
890
891
            if ($xml) {
892
                /**
893
                 * After the datasource has executed, either by itself or via the
894
                 * `DataSourcePreExecute` delegate, and if the `$xml` variable is truthy,
895
                 * this delegate allows extensions to modify the output XML and parameter pool
896
                 *
897
                 * @since Symphony 2.3
898
                 * @delegate DataSourcePostExecute
899
                 * @param string $context
900
                 * '/frontend/'
901
                 * @param DataSource $datasource
902
                 *  The Datasource object
903
                 * @param mixed $xml
904
                 *  The XML output of the data source. Can be an `XMLElement` or string.
905
                 * @param array $param_pool
906
                 *  The existing param pool including output parameters of any previous data sources
907
                 */
908
                Symphony::ExtensionManager()->notifyMembers('DataSourcePostExecute', '/frontend/', array(
909
                    'datasource' => $ds,
910
                    'xml' => &$xml,
911
                    'param_pool' => &$this->_env['pool']
912
                ));
913
914
                if ($xml instanceof XMLElement) {
915
                    $wrapper->appendChild($xml);
916
                } else {
917
                    $wrapper->appendChild(
918
                        '    ' . trim($xml) . PHP_EOL
919
                    );
920
                }
921
            }
922
923
            $queries = Symphony::Database()->queryCount() - $queries;
924
            Symphony::Profiler()->seed($startTime);
925
            Symphony::Profiler()->sample($handle, PROFILE_LAP, 'Datasource', $queries);
926
            unset($ds);
927
        }
928
    }
929
930
    /**
931
     * The function finds the correct order Datasources need to be processed in to
932
     * satisfy all dependencies that parameters can resolve correctly and in time for
933
     * other Datasources to filter on.
934
     *
935
     * @param array $dependenciesList
936
     *  An associative array with the key being the Datasource handle and the values
937
     *  being it's dependencies.
938
     * @return array
939
     *  The sorted array of Datasources in order of how they should be executed
940
     */
941
    private function __findDatasourceOrder($dependenciesList)
942
    {
943
        if (!is_array($dependenciesList) || empty($dependenciesList)) {
944
            return array();
945
        }
946
947
        foreach ($dependenciesList as $handle => $dependencies) {
948
            foreach ($dependencies as $i => $dependency) {
949
                $dependency = explode('.', $dependency);
950
                $dependenciesList[$handle][$i] = reset($dependency);
951
            }
952
        }
953
954
        $orderedList = array();
955
        $dsKeyArray = $this->__buildDatasourcePooledParamList(array_keys($dependenciesList));
956
957
        // 1. First do a cleanup of each dependency list, removing non-existant DS's and find
958
        //    the ones that have no dependencies, removing them from the list
959
        foreach ($dependenciesList as $handle => $dependencies) {
960
            $dependenciesList[$handle] = @array_intersect($dsKeyArray, $dependencies);
961
962
            if (empty($dependenciesList[$handle])) {
963
                unset($dependenciesList[$handle]);
964
                $orderedList[] = str_replace('_', '-', $handle);
965
            }
966
        }
967
968
        // 2. Iterate over the remaining DS's. Find if all their dependencies are
969
        //    in the $orderedList array. Keep iterating until all DS's are in that list
970
        //    or there are circular dependencies (list doesn't change between iterations
971
        //    of the while loop)
972
        do {
973
            $last_count = count($dependenciesList);
974
975
            foreach ($dependenciesList as $handle => $dependencies) {
976
                if (General::in_array_all(array_map(function($a) {return str_replace('$ds-', '', $a);}, $dependencies), $orderedList)) {
977
                    $orderedList[] = str_replace('_', '-', $handle);
978
                    unset($dependenciesList[$handle]);
979
                }
980
            }
981
        } while (!empty($dependenciesList) && $last_count > count($dependenciesList));
982
983
        if (!empty($dependenciesList)) {
984
            $orderedList = array_merge($orderedList, array_keys($dependenciesList));
985
        }
986
987
        return array_map(function($a) {return str_replace('-', '_', $a);}, $orderedList);
988
    }
989
990
    /**
991
     * Given an array of datasource dependancies, this function will translate
992
     * each of them to be a valid datasource handle.
993
     *
994
     * @param array $datasources
995
     *  The datasource dependencies
996
     * @return array
997
     *  An array of the handlised datasources
998
     */
999
    private function __buildDatasourcePooledParamList($datasources)
1000
    {
1001
        if (!is_array($datasources) || empty($datasources)) {
1002
            return array();
1003
        }
1004
1005
        $list = array();
1006
1007
        foreach ($datasources as $handle) {
1008
            $rootelement = str_replace('_', '-', $handle);
1009
            $list[] = '$ds-' . $rootelement;
1010
        }
1011
1012
        return $list;
1013
    }
1014
1015
    /**
1016
     * Given a string (expected to be a URL parameter) this function will
1017
     * ensure it is safe to embed in an XML document.
1018
     *
1019
     * @since Symphony 2.3.1
1020
     * @param string $parameter
1021
     *  The string to sanitize for XML
1022
     * @return string
1023
     *  The sanitized string
1024
     */
1025
    public static function sanitizeParameter($parameter)
1026
    {
1027
        return XMLElement::stripInvalidXMLCharacters($parameter);
1028
    }
1029
}
1030