Completed
Push — master ( 32a670...32a37c )
by Damian
37s queued 21s
created

src/Control/Controller.php (12 issues)

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Dev\Debug;
7
use SilverStripe\Dev\Deprecation;
8
use SilverStripe\ORM\FieldType\DBHTMLText;
9
use SilverStripe\Security\BasicAuth;
10
use SilverStripe\Security\BasicAuthMiddleware;
11
use SilverStripe\Security\Member;
12
use SilverStripe\Security\Security;
13
use SilverStripe\View\SSViewer;
14
use SilverStripe\View\TemplateGlobalProvider;
15
16
/**
17
 * Controllers are the cornerstone of all site functionality in SilverStripe. The {@link Director}
18
 * selects a controller to pass control to, and then calls {@link handleRequest()}. This method will execute
19
 * the appropriate action - either by calling the action method, or displaying the action's template.
20
 *
21
 * See {@link getTemplate()} for information on how the template is chosen.
22
 */
23
class Controller extends RequestHandler implements TemplateGlobalProvider
24
{
25
26
    /**
27
     * An array of arguments extracted from the URL.
28
     *
29
     * @var array
30
     */
31
    protected $urlParams;
32
33
    /**
34
     * Contains all GET and POST parameters passed to the current {@link HTTPRequest}.
35
     *
36
     * @var array
37
     */
38
    protected $requestParams;
39
40
    /**
41
     * The URL part matched on the current controller as determined by the "$Action" part of the
42
     * {@link $url_handlers} definition. Should correlate to a public method on this controller.
43
     *
44
     * Used in {@link render()} and {@link getViewer()} to determine action-specific templates.
45
     *
46
     * @var string
47
     */
48
    protected $action;
49
50
    /**
51
     * Stack of current controllers. Controller::$controller_stack[0] is the current controller.
52
     *
53
     * @var array
54
     */
55
    protected static $controller_stack = array();
56
57
    /**
58
     * Assign templates for this controller.
59
     * Map of action => template name
60
     *
61
     * @var array
62
     */
63
    protected $templates = [];
64
65
    /**
66
     * @deprecated 4.1.0:5.0.0 Add this controller's url to
67
     * SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false
68
     * @var bool
69
     */
70
    protected $basicAuthEnabled = true;
71
72
    /**
73
     * The response object that the controller returns.
74
     *
75
     * Set in {@link handleRequest()}.
76
     *
77
     * @var HTTPResponse
78
     */
79
    protected $response;
80
81
    /**
82
     * Default URL handlers.
83
     *
84
     * @var array
85
     */
86
    private static $url_handlers = array(
0 ignored issues
show
The private property $url_handlers is not used, and could be removed.
Loading history...
87
        '$Action//$ID/$OtherID' => 'handleAction',
88
    );
89
90
    /**
91
     * @var array
92
     */
93
    private static $allowed_actions = array(
0 ignored issues
show
The private property $allowed_actions is not used, and could be removed.
Loading history...
94
        'handleAction',
95
        'handleIndex',
96
    );
97
98
    /**
99
     * Initialisation function that is run before any action on the controller is called.
100
     *
101
     * @uses BasicAuth::requireLogin()
102
     */
103
    protected function init()
104
    {
105
        // @todo This will be removed in 5.0 and will be controlled by middleware instead
106
        if ($this->basicAuthEnabled) {
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Control\Controller::$basicAuthEnabled has been deprecated: 4.1.0:5.0.0 Add this controller's url to SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false ( Ignorable by Annotation )

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

106
        if (/** @scrutinizer ignore-deprecated */ $this->basicAuthEnabled) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
107
            BasicAuth::protect_site_if_necessary();
108
        }
109
110
        // This is used to test that subordinate controllers are actually calling parent::init() - a common bug
111
        $this->baseInitCalled = true;
112
    }
113
114
    /**
115
     * A stand in function to protect the init function from failing to be called as well as providing before and
116
     * after hooks for the init function itself
117
     *
118
     * This should be called on all controllers before handling requests
119
     */
120
    public function doInit()
121
    {
122
        //extension hook
123
        $this->extend('onBeforeInit');
124
125
        // Safety call
126
        $this->baseInitCalled = false;
127
        $this->init();
128
        if (!$this->baseInitCalled) {
129
            $class = static::class;
130
            user_error(
131
                "init() method on class '{$class}' doesn't call Controller::init()."
132
                . "Make sure that you have parent::init() included.",
133
                E_USER_WARNING
134
            );
135
        }
136
137
        $this->extend('onAfterInit');
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     *
143
     * Also set the URLParams
144
     */
145
    public function setRequest($request)
146
    {
147
        $return = parent::setRequest($request);
148
        $this->setURLParams($this->getRequest()->allParams());
149
150
        return $return;
151
    }
152
153
    /**
154
     * A bootstrap for the handleRequest method
155
     *
156
     * @todo setDataModel and setRequest are redundantly called in parent::handleRequest() - sort this out
157
     *
158
     * @param HTTPRequest $request
159
     */
160
    protected function beforeHandleRequest(HTTPRequest $request)
161
    {
162
        //Set up the internal dependencies (request, response)
163
        $this->setRequest($request);
164
        //Push the current controller to protect against weird session issues
165
        $this->pushCurrent();
166
        $this->setResponse(new HTTPResponse());
167
        //kick off the init functionality
168
        $this->doInit();
169
    }
170
171
    /**
172
     * Cleanup for the handleRequest method
173
     */
174
    protected function afterHandleRequest()
175
    {
176
        //Pop the current controller from the stack
177
        $this->popCurrent();
178
    }
179
180
    /**
181
     * Executes this controller, and return an {@link HTTPResponse} object with the result.
182
     *
183
     * This method defers to {@link RequestHandler->handleRequest()} to determine which action
184
     *    should be executed
185
     *
186
     * Note: You should rarely need to overload handleRequest() -
187
     * this kind of change is only really appropriate for things like nested
188
     * controllers - {@link ModelAsController} and {@link RootURLController}
189
     * are two examples here.  If you want to make more
190
     * orthodox functionality, it's better to overload {@link init()} or {@link index()}.
191
     *
192
     * Important: If you are going to overload handleRequest,
193
     * make sure that you start the method with $this->beforeHandleRequest()
194
     * and end the method with $this->afterHandleRequest()
195
     *
196
     * @param HTTPRequest $request
197
     * @return HTTPResponse
198
     */
199
    public function handleRequest(HTTPRequest $request)
200
    {
201
        if (!$request) {
0 ignored issues
show
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
202
            user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
203
        }
204
205
        //set up the controller for the incoming request
206
        $this->beforeHandleRequest($request);
207
208
        //if the before handler manipulated the response in a way that we shouldn't proceed, then skip our request
209
        // handling
210
        if (!$this->getResponse()->isFinished()) {
211
            //retrieve the response for the request
212
            $response = parent::handleRequest($request);
213
214
            //prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses)
215
            $this->prepareResponse($response);
216
        }
217
218
        //after request work
219
        $this->afterHandleRequest();
220
221
        //return the response
222
        return $this->getResponse();
223
    }
224
225
    /**
226
     * Prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses) and
227
     * changes the controller response object appropriately
228
     *
229
     * @param HTTPResponse|Object $response
230
     */
231
    protected function prepareResponse($response)
232
    {
233
        if ($response instanceof HTTPResponse) {
234
            if (isset($_REQUEST['debug_request'])) {
235
                $class = static::class;
236
                Debug::message(
237
                    "Request handler returned HTTPResponse object to {$class} controller;"
238
                    . "returning it without modification."
239
                );
240
            }
241
            $this->setResponse($response);
242
        } else {
243
            // Could be Controller, or ViewableData_Customised controller wrapper
244
            if (ClassInfo::hasMethod($response, 'getViewer')) {
245
                if (isset($_REQUEST['debug_request'])) {
246
                    $class = static::class;
247
                    $responseClass = get_class($response);
248
                    Debug::message(
249
                        "Request handler {$responseClass} object to {$class} controller;"
250
                        . "rendering with template returned by {$responseClass}::getViewer()"
251
                    );
252
                }
253
                $response = $response->getViewer($this->getAction())->process($response);
254
            }
255
256
            $this->getResponse()->setBody($response);
257
        }
258
259
        //deal with content if appropriate
260
        ContentNegotiator::process($this->getResponse());
261
    }
262
263
    /**
264
     * Controller's default action handler.  It will call the method named in "$Action", if that method
265
     * exists. If "$Action" isn't given, it will use "index" as a default.
266
     *
267
     * @param HTTPRequest $request
268
     * @param string $action
269
     *
270
     * @return DBHTMLText|HTTPResponse
271
     */
272
    protected function handleAction($request, $action)
273
    {
274
        foreach ($request->latestParams() as $k => $v) {
275
            if ($v || !isset($this->urlParams[$k])) {
276
                $this->urlParams[$k] = $v;
277
            }
278
        }
279
280
        $this->action = $action;
281
        $this->requestParams = $request->requestVars();
282
283
        if ($this->hasMethod($action)) {
284
            $result = parent::handleAction($request, $action);
285
286
            // If the action returns an array, customise with it before rendering the template.
287
            if (is_array($result)) {
0 ignored issues
show
The condition is_array($result) is always false.
Loading history...
288
                return $this->getViewer($action)->process($this->customise($result));
289
            } else {
290
                return $result;
291
            }
292
        }
293
294
        // Fall back to index action with before/after handlers
295
        $beforeResult = $this->extend('beforeCallActionHandler', $request, $action);
296
        if ($beforeResult) {
297
            return reset($beforeResult);
298
        }
299
300
        $result = $this->getViewer($action)->process($this);
301
302
        $afterResult = $this->extend('afterCallActionHandler', $request, $action, $result);
303
        if ($afterResult) {
304
            return reset($afterResult);
305
        }
306
307
        return $result;
308
    }
309
310
    /**
311
     * @param array $urlParams
312
     * @return $this
313
     */
314
    public function setURLParams($urlParams)
315
    {
316
        $this->urlParams = $urlParams;
317
        return $this;
318
    }
319
320
    /**
321
     * Returns the parameters extracted from the URL by the {@link Director}.
322
     *
323
     * @return array
324
     */
325
    public function getURLParams()
326
    {
327
        return $this->urlParams;
328
    }
329
330
    /**
331
     * Returns the HTTPResponse object that this controller is building up. Can be used to set the
332
     * status code and headers.
333
     *
334
     * @return HTTPResponse
335
     */
336
    public function getResponse()
337
    {
338
        if (!$this->response) {
339
            $this->setResponse(new HTTPResponse());
340
        }
341
        return $this->response;
342
    }
343
344
    /**
345
     * Sets the HTTPResponse object that this controller is building up.
346
     *
347
     * @param HTTPResponse $response
348
     *
349
     * @return $this
350
     */
351
    public function setResponse(HTTPResponse $response)
352
    {
353
        $this->response = $response;
354
        return $this;
355
    }
356
357
    /**
358
     * @var bool
359
     */
360
    protected $baseInitCalled = false;
361
362
    /**
363
     * This is the default action handler used if a method doesn't exist. It will process the
364
     * controller object with the template returned by {@link getViewer()}.
365
     *
366
     * @param string $action
367
     * @return DBHTMLText
368
     */
369
    public function defaultAction($action)
370
    {
371
        return $this->getViewer($action)->process($this);
372
    }
373
374
    /**
375
     * Returns the action that is being executed on this controller.
376
     *
377
     * @return string
378
     */
379
    public function getAction()
380
    {
381
        return $this->action;
382
    }
383
384
    /**
385
     * Return the viewer identified being the default handler for this Controller/Action combination.
386
     *
387
     * @param string $action
388
     *
389
     * @return SSViewer
390
     */
391
    public function getViewer($action)
392
    {
393
        // Hard-coded templates
394
        if (isset($this->templates[$action]) && $this->templates[$action]) {
395
            $templates = $this->templates[$action];
396
        } elseif (isset($this->templates['index']) && $this->templates['index']) {
397
            $templates = $this->templates['index'];
398
        } elseif ($this->template) {
0 ignored issues
show
Bug Best Practice introduced by
The property template does not exist on SilverStripe\Control\Controller. Since you implemented __get, consider adding a @property annotation.
Loading history...
399
            $templates = $this->template;
400
        } else {
401
            // Build templates based on class hierarchy
402
            $actionTemplates = [];
403
            $classTemplates = [];
404
            $parentClass = static::class;
405
            while ($parentClass !== parent::class) {
406
                // _action templates have higher priority
407
                if ($action && $action != 'index') {
408
                    $actionTemplates[] = strtok($parentClass, '_') . '_' . $action;
409
                }
410
                // class templates have lower priority
411
                $classTemplates[] = strtok($parentClass, '_');
412
                $parentClass = get_parent_class($parentClass);
413
            }
414
415
            // Add controller templates for inheritance chain
416
            $templates = array_unique(array_merge($actionTemplates, $classTemplates));
417
        }
418
419
        return SSViewer::create($templates);
420
    }
421
422
    /**
423
     * @param string $action
424
     *
425
     * @return bool
426
     */
427
    public function hasAction($action)
428
    {
429
        return parent::hasAction($action) || $this->hasActionTemplate($action);
430
    }
431
432
    /**
433
     * Removes all the "action" part of the current URL and returns the result. If no action parameter
434
     * is present, returns the full URL.
435
     *
436
     * @param string $fullURL
437
     * @param null|string $action
438
     *
439
     * @return string
440
     */
441
    public function removeAction($fullURL, $action = null)
442
    {
443
        if (!$action) {
444
            $action = $this->getAction();    //default to current action
445
        }
446
        $returnURL = $fullURL;
447
448
        if (($pos = strpos($fullURL, $action)) !== false) {
449
            $returnURL = substr($fullURL, 0, $pos);
450
        }
451
452
        return $returnURL;
453
    }
454
455
    /**
456
     * Return the class that defines the given action, so that we know where to check allowed_actions.
457
     * Overrides RequestHandler to also look at defined templates.
458
     *
459
     * @param string $action
460
     *
461
     * @return string
462
     */
463
    protected function definingClassForAction($action)
464
    {
465
        $definingClass = parent::definingClassForAction($action);
466
        if ($definingClass) {
467
            return $definingClass;
468
        }
469
470
        $class = static::class;
471
        while ($class != 'SilverStripe\\Control\\RequestHandler') {
472
            $templateName = strtok($class, '_') . '_' . $action;
473
            if (SSViewer::hasTemplate($templateName)) {
474
                return $class;
475
            }
476
477
            $class = get_parent_class($class);
478
        }
479
480
        return null;
481
    }
482
483
    /**
484
     * Returns TRUE if this controller has a template that is specifically designed to handle a
485
     * specific action.
486
     *
487
     * @param string $action
488
     *
489
     * @return bool
490
     */
491
    public function hasActionTemplate($action)
492
    {
493
        if (isset($this->templates[$action])) {
494
            return true;
495
        }
496
497
        $parentClass = static::class;
498
        $templates   = array();
499
500
        while ($parentClass != __CLASS__) {
501
            $templates[] = strtok($parentClass, '_') . '_' . $action;
502
            $parentClass = get_parent_class($parentClass);
503
        }
504
505
        return SSViewer::hasTemplate($templates);
506
    }
507
508
    /**
509
     * Render the current controller with the templates determined by {@link getViewer()}.
510
     *
511
     * @param array $params
512
     *
513
     * @return string
514
     */
515
    public function render($params = null)
516
    {
517
        $template = $this->getViewer($this->getAction());
518
519
        // if the object is already customised (e.g. through Controller->run()), use it
520
        $obj = $this->getCustomisedObj() ?: $this;
521
522
        if ($params) {
523
            $obj = $this->customise($params);
524
        }
525
526
        return $template->process($obj);
527
    }
528
529
    /**
530
     * Call this to disable site-wide basic authentication for a specific controller. This must be
531
     * called before Controller::init(). That is, you must call it in your controller's init method
532
     * before it calls parent::init().
533
     *
534
     * @deprecated 4.1.0:5.0.0 Add this controller's url to
535
     * SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false
536
     */
537
    public function disableBasicAuth()
538
    {
539
        Deprecation::notice(
540
            '5.0',
541
            'Add this controller\'s url to ' . BasicAuthMiddleware::class . '.URLPatterns injected property instead'
542
        );
543
        $this->basicAuthEnabled = false;
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Control\Controller::$basicAuthEnabled has been deprecated: 4.1.0:5.0.0 Add this controller's url to SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false ( Ignorable by Annotation )

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

543
        /** @scrutinizer ignore-deprecated */ $this->basicAuthEnabled = false;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
544
    }
545
546
    /**
547
     * Returns the current controller.
548
     *
549
     * @return Controller
550
     */
551
    public static function curr()
552
    {
553
        if (Controller::$controller_stack) {
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
Bug Best Practice introduced by
The expression SilverStripe\Control\Controller::controller_stack of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
554
            return Controller::$controller_stack[0];
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
555
        }
556
        user_error("No current controller available", E_USER_WARNING);
557
        return null;
558
    }
559
560
    /**
561
     * Tests whether we have a currently active controller or not. True if there is at least 1
562
     * controller in the stack.
563
     *
564
     * @return bool
565
     */
566
    public static function has_curr()
567
    {
568
        return Controller::$controller_stack ? true : false;
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
569
    }
570
571
    /**
572
     * Returns true if the member is allowed to do the given action. Defaults to the currently logged
573
     * in user.
574
     *
575
     * @param string $perm
576
     * @param null|member $member
577
     *
578
     * @return bool
579
     */
580
    public function can($perm, $member = null)
581
    {
582
        if (!$member) {
583
            $member = Security::getCurrentUser();
584
        }
585
        if (is_array($perm)) {
0 ignored issues
show
The condition is_array($perm) is always false.
Loading history...
586
            $perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member));
587
            return min($perm);
588
        }
589
        if ($this->hasMethod($methodName = 'can' . $perm)) {
590
            return $this->$methodName($member);
591
        } else {
592
            return true;
593
        }
594
    }
595
596
    /**
597
     * Pushes this controller onto the stack of current controllers. This means that any redirection,
598
     * session setting, or other things that rely on Controller::curr() will now write to this
599
     * controller object.
600
     *
601
     * Note: Ensure this controller is assigned a request with a valid session before pushing
602
     * it to the stack.
603
     */
604
    public function pushCurrent()
605
    {
606
        // Ensure this controller has a valid session
607
        $this->getRequest()->getSession();
608
        array_unshift(self::$controller_stack, $this);
609
    }
610
611
    /**
612
     * Pop this controller off the top of the stack.
613
     */
614
    public function popCurrent()
615
    {
616
        if ($this === self::$controller_stack[0]) {
617
            array_shift(self::$controller_stack);
618
        } else {
619
            $class = static::class;
620
            user_error(
621
                "popCurrent called on {$class} controller, but it wasn't at the top of the stack",
622
                E_USER_WARNING
623
            );
624
        }
625
    }
626
627
    /**
628
     * Redirect to the given URL.
629
     *
630
     * @param string $url
631
     * @param int $code
632
     * @return HTTPResponse
633
     */
634
    public function redirect($url, $code = 302)
635
    {
636
        if ($this->getResponse()->getHeader('Location') && $this->getResponse()->getHeader('Location') != $url) {
637
            user_error("Already directed to " . $this->getResponse()->getHeader('Location')
638
                . "; now trying to direct to $url", E_USER_WARNING);
639
            return null;
640
        }
641
        $response = parent::redirect($url, $code);
642
        $this->setResponse($response);
643
        return $response;
644
    }
645
646
    /**
647
     * Tests whether a redirection has been requested. If redirect() has been called, it will return
648
     * the URL redirected to. Otherwise, it will return null.
649
     *
650
     * @return null|string
651
     */
652
    public function redirectedTo()
653
    {
654
        return $this->getResponse() && $this->getResponse()->getHeader('Location');
655
    }
656
657
    /**
658
     * Joins two or more link segments together, putting a slash between them if necessary. Use this
659
     * for building the results of {@link Link()} methods. If either of the links have query strings,
660
     * then they will be combined and put at the end of the resulting url.
661
     *
662
     * Caution: All parameters are expected to be URI-encoded already.
663
     *
664
     * @param string|array $arg,.. One or more link segments, or list of link segments as an array
665
     * @return string
666
     */
667
    public static function join_links($arg = null)
668
    {
669
        if (func_num_args() === 1 && is_array($arg)) {
670
            $args = $arg;
671
        } else {
672
            $args = func_get_args();
673
        }
674
        $result = "";
675
        $queryargs = array();
676
        $fragmentIdentifier = null;
677
678
        foreach ($args as $arg) {
679
            // Find fragment identifier - keep the last one
680
            if (strpos($arg, '#') !== false) {
681
                list($arg, $fragmentIdentifier) = explode('#', $arg, 2);
682
            }
683
            // Find querystrings
684
            if (strpos($arg, '?') !== false) {
685
                list($arg, $suffix) = explode('?', $arg, 2);
686
                parse_str($suffix, $localargs);
687
                $queryargs = array_merge($queryargs, $localargs);
688
            }
689
            if ((is_string($arg) && $arg) || is_numeric($arg)) {
690
                $arg = (string) $arg;
691
                if ($result && substr($result, -1) != '/' && $arg[0] != '/') {
692
                    $result .= "/$arg";
693
                } else {
694
                    $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
695
                }
696
            }
697
        }
698
699
        if ($queryargs) {
700
            $result .= '?' . http_build_query($queryargs);
701
        }
702
703
        if ($fragmentIdentifier) {
704
            $result .= "#$fragmentIdentifier";
705
        }
706
707
        return $result;
708
    }
709
710
    /**
711
     * @return array
712
     */
713
    public static function get_template_global_variables()
714
    {
715
        return array(
716
            'CurrentPage' => 'curr',
717
        );
718
    }
719
}
720