Completed
Push — php7-support ( ec5817...a3ee17 )
by Sam
09:34 queued 03:20
created

Controller   D

Complexity

Total Complexity 104

Size/Duplication

Total Lines 670
Duplicated Lines 1.79 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 104
c 1
b 1
f 0
lcom 1
cbo 11
dl 12
loc 670
rs 4.1641

30 Methods

Rating   Name   Duplication   Size   Complexity  
A Link() 0 3 1
A init() 0 6 2
B handleRequest() 0 56 9
C handleAction() 0 34 8
A setURLParams() 0 3 1
A getURLParams() 0 3 1
A getResponse() 0 6 2
A setResponse() 0 4 1
A getFormOwner() 0 14 3
A defaultAction() 0 3 1
A getAction() 0 3 1
D getViewer() 8 36 10
A hasAction() 0 3 2
A removeAction() 0 10 3
A definingClassForAction() 0 12 4
A hasActionTemplate() 4 13 3
A render() 0 10 3
A disableBasicAuth() 0 3 1
A curr() 0 7 2
A has_curr() 0 3 2
A can() 0 12 4
A pushCurrent() 0 11 3
A popCurrent() 0 8 2
B redirect() 0 15 8
C redirectBack() 0 30 8
A redirectedTo() 0 3 2
A getSession() 0 3 1
A setSession() 0 3 1
C join_links() 0 30 14
A get_template_global_variables() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Controllers are the cornerstone of all site functionality in SilverStripe. The {@link Director}
4
 * selects a controller to pass control to, and then calls {@link run()}. This method will execute
5
 * the appropriate action - either by calling the action method, or displaying the action's template.
6
 *
7
 * See {@link getTemplate()} for information on how the template is chosen.
8
 *
9
 * @package framework
10
 *
11
 * @subpackage control
12
 */
13
class Controller extends RequestHandler implements TemplateGlobalProvider {
14
15
	/**
16
	 * An array of arguments extracted from the URL.
17
	 *
18
	 * @var array
19
	 */
20
	protected $urlParams;
21
22
	/**
23
	 * Contains all GET and POST parameters passed to the current {@link SS_HTTPRequest}.
24
	 *
25
	 * @var array
26
	 */
27
	protected $requestParams;
28
29
	/**
30
	 * The URL part matched on the current controller as determined by the "$Action" part of the
31
	 * {@link $url_handlers} definition. Should correlate to a public method on this controller.
32
	 *
33
	 * Used in {@link render()} and {@link getViewer()} to determine action-specific templates.
34
	 *
35
	 * @var string
36
	 */
37
	protected $action;
38
39
	/**
40
	 * The {@link Session} object for this controller.
41
	 *
42
	 * @var Session
43
	 */
44
	protected $session;
45
46
	/**
47
	 * Stack of current controllers. Controller::$controller_stack[0] is the current controller.
48
	 *
49
	 * @var array
50
	 */
51
	protected static $controller_stack = array();
52
53
	/**
54
	 * @var bool
55
	 */
56
	protected $basicAuthEnabled = true;
57
58
	/**
59
	 * The response object that the controller returns.
60
	 *
61
	 * Set in {@link handleRequest()}.
62
	 *
63
	 * @var SS_HTTPResponse
64
	 */
65
	protected $response;
66
67
	/**
68
	 * Default URL handlers.
69
	 *
70
	 * @var array
71
	 */
72
	private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
73
		'$Action//$ID/$OtherID' => 'handleAction',
74
	);
75
76
	/**
77
	 * @var array
78
	 */
79
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
80
		'handleAction',
81
		'handleIndex',
82
	);
83
84
	/**
85
	 * Initialisation function that is run before any action on the controller is called.
86
	 *
87
	 * @uses BasicAuth::requireLogin()
88
	 */
89
	public function init() {
90
		if($this->basicAuthEnabled) BasicAuth::protect_site_if_necessary();
91
92
		// This is used to test that subordinate controllers are actually calling parent::init() - a common bug
93
		$this->baseInitCalled = true;
94
	}
95
96
	/**
97
	 * Returns a link to this controller. Overload with your own Link rules if they exist.
98
	 *
99
	 * @return string
100
	 */
101
	public function Link() {
102
		return get_class($this) .'/';
103
	}
104
105
	/**
106
	 * Executes this controller, and return an {@link SS_HTTPResponse} object with the result.
107
	 *
108
	 * This method first does a few set-up activities:
109
	 * - Push this controller ont to the controller stack - see {@link Controller::curr()} for
110
	 *   information about this.
111
	 * - Call {@link init()}
112
	 * - Defer to {@link RequestHandler->handleRequest()} to determine which action should be executed.
113
	 *
114
	 * Note: $requestParams['executeForm'] support was removed, make the following change in your URLs:
115
	 * "/?executeForm=FooBar" -> "/FooBar".
116
	 *
117
	 * Also make sure "FooBar" is in the $allowed_actions of your controller class.
118
	 *
119
	 * Note: You should rarely need to overload run() - this kind of change is only really appropriate
120
	 * for things like nested controllers - {@link ModelAsController} and {@link RootURLController}
121
	 * are two examples here. If you want to make more orthodox functionality, it's better to overload
122
	 * {@link init()} or {@link index()}.
123
	 *
124
	 * Important: If you are going to overload handleRequest, make sure that you start the method with
125
	 * $this->pushCurrent() and end the method with $this->popCurrent(). Failure to do this will create
126
	 * weird session errors.
127
	 *
128
	 * @param SS_HTTPRequest $request
129
	 * @param DataModel $model
130
	 *
131
	 * @return SS_HTTPResponse
132
	 */
133
	public function handleRequest(SS_HTTPRequest $request, DataModel $model) {
134
		if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
135
136
		$this->pushCurrent();
137
		$this->urlParams = $request->allParams();
138
		$this->setRequest($request);
139
		$this->getResponse();
140
		$this->setDataModel($model);
141
142
		$this->extend('onBeforeInit');
143
144
		// Init
145
		$this->baseInitCalled = false;
146
		$this->init();
147
		if(!$this->baseInitCalled) {
148
			user_error("init() method on class '$this->class' doesn't call Controller::init()."
149
				. "Make sure that you have parent::init() included.", E_USER_WARNING);
150
		}
151
152
		$this->extend('onAfterInit');
153
154
		$response = $this->getResponse();
155
		// If we had a redirection or something, halt processing.
156
		if($response->isFinished()) {
157
			$this->popCurrent();
158
			return $response;
159
		}
160
161
		$body = parent::handleRequest($request, $model);
162
		if($body instanceof SS_HTTPResponse) {
163
			if(isset($_REQUEST['debug_request'])) {
164
				Debug::message("Request handler returned SS_HTTPResponse object to $this->class controller;"
165
					. "returning it without modification.");
166
			}
167
			$response = $body;
168
			$this->setResponse($response);
169
170
		} else {
171
			if($body instanceof Object && $body->hasMethod('getViewer')) {
172
				if(isset($_REQUEST['debug_request'])) {
173
					Debug::message("Request handler $body->class object to $this->class controller;"
174
						. "rendering with template returned by $body->class::getViewer()");
175
				}
176
				$body = $body->getViewer($this->getAction())->process($body);
177
			}
178
179
			$response->setBody($body);
180
		}
181
182
183
		ContentNegotiator::process($response);
184
		HTTP::add_cache_headers($response);
185
186
		$this->popCurrent();
187
		return $response;
188
	}
189
190
	/**
191
	 * Controller's default action handler.  It will call the method named in "$Action", if that method
192
	 * exists. If "$Action" isn't given, it will use "index" as a default.
193
	 *
194
	 * @param SS_HTTPRequest $request
195
	 * @param string $action
196
	 *
197
	 * @return HTMLText|SS_HTTPResponse
198
	 */
199
	protected function handleAction($request, $action) {
200
		foreach($request->latestParams() as $k => $v) {
201
			if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
202
		}
203
204
		$this->action = $action;
205
		$this->requestParams = $request->requestVars();
0 ignored issues
show
Documentation Bug introduced by
It seems like $request->requestVars() can be null. However, the property $requestParams is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
206
207
		if($this->hasMethod($action)) {
208
			$result = parent::handleAction($request, $action);
209
210
			// If the action returns an array, customise with it before rendering the template.
211
			if(is_array($result)) {
212
				return $this->getViewer($action)->process($this->customise($result));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getViewer(...s->customise($result)); (SilverStripe\Model\FieldType\DBField) is incompatible with the return type documented by Controller::handleAction of type HTMLText|SS_HTTPResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
213
			} else {
214
				return $result;
215
			}
216
		}
217
218
		// Fall back to index action with before/after handlers
219
		$beforeResult = $this->extend('beforeCallActionHandler', $request, $action);
220
		if ($beforeResult) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $beforeResult 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...
221
			return reset($beforeResult);
222
		}
223
224
		$result = $this->getViewer($action)->process($this);
225
226
		$afterResult = $this->extend('afterCallActionHandler', $request, $action, $result);
227
		if($afterResult) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $afterResult 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...
228
			return reset($afterResult);
229
		}
230
231
		return $result;
232
	}
233
234
	/**
235
	 * @param array $urlParams
236
	 */
237
	public function setURLParams($urlParams) {
238
		$this->urlParams = $urlParams;
239
	}
240
241
	/**
242
	 * Returns the parameters extracted from the URL by the {@link Director}.
243
	 *
244
	 * @return array
245
	 */
246
	public function getURLParams() {
247
		return $this->urlParams;
248
	}
249
250
	/**
251
	 * Returns the SS_HTTPResponse object that this controller is building up. Can be used to set the
252
	 * status code and headers.
253
	 *
254
	 * @return SS_HTTPResponse
255
	 */
256
	public function getResponse() {
257
		if (!$this->response) {
258
			$this->setResponse(new SS_HTTPResponse());
259
		}
260
		return $this->response;
261
	}
262
263
	/**
264
	 * Sets the SS_HTTPResponse object that this controller is building up.
265
	 *
266
	 * @param SS_HTTPResponse $response
267
	 *
268
	 * @return $this
269
	 */
270
	public function setResponse(SS_HTTPResponse $response) {
271
		$this->response = $response;
272
		return $this;
273
	}
274
275
	/**
276
	 * @var bool
277
	 */
278
	protected $baseInitCalled = false;
279
280
	/**
281
	 * Return the object that is going to own a form that's being processed, and handle its execution.
282
	 * Note that the result need not be an actual controller object.
283
	 *
284
	 * @return mixed
285
	 */
286
	public function getFormOwner() {
287
		// Get the appropriate controller: sometimes we want to get a form from another controller
288
		if(isset($this->requestParams['formController'])) {
289
			$formController = Director::getControllerForURL($this->requestParams['formController']);
290
291
			while(is_a($formController, 'NestedController')) {
292
				$formController = $formController->getNestedController();
293
			}
294
			return $formController;
295
296
		} else {
297
			return $this;
298
		}
299
	}
300
301
	/**
302
	 * This is the default action handler used if a method doesn't exist. It will process the
303
	 * controller object with the template returned by {@link getViewer()}.
304
	 *
305
	 * @param string $action
306
	 *
307
	 * @return HTMLText
308
	 */
309
	public function defaultAction($action) {
310
		return $this->getViewer($action)->process($this);
311
	}
312
313
	/**
314
	 * Returns the action that is being executed on this controller.
315
	 *
316
	 * @return string
317
	 */
318
	public function getAction() {
319
		return $this->action;
320
	}
321
322
	/**
323
	 * Return the viewer identified being the default handler for this Controller/Action combination.
324
	 *
325
	 * @param string $action
326
	 *
327
	 * @return SSViewer
328
	 */
329
	public function getViewer($action) {
330
		// Hard-coded templates
331
		if(isset($this->templates[$action]) && $this->templates[$action]) {
332
			$templates = $this->templates[$action];
333
334
		}	else if(isset($this->templates['index']) && $this->templates['index']) {
335
			$templates = $this->templates['index'];
336
337
		}	else if($this->template) {
338
			$templates = $this->template;
0 ignored issues
show
Documentation introduced by
The property template does not exist on object<Controller>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
339
		} else {
340
			// Add action-specific templates for inheritance chain
341
			$templates = array();
342
			$parentClass = $this->class;
0 ignored issues
show
Unused Code introduced by
$parentClass is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
343
			if($action && $action != 'index') {
344
				$parentClass = $this->class;
345 View Code Duplication
				while($parentClass != "Controller") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
346
					$templates[] = strtok($parentClass,'_') . '_' . $action;
347
					$parentClass = get_parent_class($parentClass);
348
				}
349
			}
350
			// Add controller templates for inheritance chain
351
			$parentClass = $this->class;
352 View Code Duplication
			while($parentClass != "Controller") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
353
				$templates[] = strtok($parentClass,'_');
354
				$parentClass = get_parent_class($parentClass);
355
			}
356
357
			$templates[] = 'Controller';
358
359
			// remove duplicates
360
			$templates = array_unique($templates);
361
		}
362
363
		return new SSViewer($templates);
364
	}
365
366
	/**
367
	 * @param string $action
368
	 *
369
	 * @return bool
370
	 */
371
	public function hasAction($action) {
372
		return parent::hasAction($action) || $this->hasActionTemplate($action);
373
	}
374
375
	/**
376
	 * Removes all the "action" part of the current URL and returns the result. If no action parameter
377
	 * is present, returns the full URL.
378
	 *
379
	 * @param string $fullURL
380
	 * @param null|string $action
381
	 *
382
	 * @return string
383
	 */
384
	public function removeAction($fullURL, $action = null) {
385
		if (!$action) $action = $this->getAction();    //default to current action
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type null|string is loosely compared to false; 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...
386
		$returnURL = $fullURL;
387
388
		if (($pos = strpos($fullURL, $action)) !== false) {
389
			$returnURL = substr($fullURL,0,$pos);
390
		}
391
392
		return $returnURL;
393
	}
394
395
	/**
396
	 * Return the class that defines the given action, so that we know where to check allowed_actions.
397
	 * Overrides RequestHandler to also look at defined templates.
398
	 *
399
	 * @param string $action
400
	 *
401
	 * @return string
402
	 */
403
	protected function definingClassForAction($action) {
404
		$definingClass = parent::definingClassForAction($action);
405
		if($definingClass) return $definingClass;
406
407
		$class = get_class($this);
408
		while($class != 'RequestHandler') {
409
			$templateName = strtok($class, '_') . '_' . $action;
410
			if(SSViewer::hasTemplate($templateName)) return $class;
0 ignored issues
show
Documentation introduced by
$templateName is of type string, but the function expects a array.

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...
411
412
			$class = get_parent_class($class);
413
		}
414
	}
415
416
	/**
417
	 * Returns TRUE if this controller has a template that is specifically designed to handle a
418
	 * specific action.
419
	 *
420
	 * @param string $action
421
	 *
422
	 * @return bool
423
	 */
424
	public function hasActionTemplate($action) {
425
		if(isset($this->templates[$action])) return true;
426
427
		$parentClass = $this->class;
428
		$templates   = array();
429
430 View Code Duplication
		while($parentClass != 'Controller') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
431
			$templates[] = strtok($parentClass, '_') . '_' . $action;
432
			$parentClass = get_parent_class($parentClass);
433
		}
434
435
		return SSViewer::hasTemplate($templates);
436
	}
437
438
	/**
439
	 * Render the current controller with the templates determined by {@link getViewer()}.
440
	 *
441
	 * @param array $params
442
	 *
443
	 * @return string
444
	 */
445
	public function render($params = null) {
446
		$template = $this->getViewer($this->getAction());
447
448
		// if the object is already customised (e.g. through Controller->run()), use it
449
		$obj = ($this->customisedObj) ? $this->customisedObj : $this;
0 ignored issues
show
Bug introduced by
The property customisedObj does not seem to exist. Did you mean customisedObject?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
450
451
		if($params) $obj = $this->customise($params);
452
453
		return $template->process($obj);
454
	}
455
456
	/**
457
	 * Call this to disable site-wide basic authentication for a specific controller. This must be
458
	 * called before Controller::init(). That is, you must call it in your controller's init method
459
	 * before it calls parent::init().
460
	 */
461
	public function disableBasicAuth() {
462
		$this->basicAuthEnabled = false;
463
	}
464
465
	/**
466
	 * Returns the current controller.
467
	 *
468
	 * @return Controller
469
	 */
470
	public static function curr() {
471
		if(Controller::$controller_stack) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \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...
472
			return Controller::$controller_stack[0];
473
		} else {
474
			user_error("No current controller available", E_USER_WARNING);
475
		}
476
	}
477
478
	/**
479
	 * Tests whether we have a currently active controller or not. True if there is at least 1
480
	 * controller in the stack.
481
	 *
482
	 * @return bool
483
	 */
484
	public static function has_curr() {
485
		return Controller::$controller_stack ? true : false;
486
	}
487
488
	/**
489
	 * Returns true if the member is allowed to do the given action. Defaults to the currently logged
490
	 * in user.
491
	 *
492
	 * @param string $perm
493
	 * @param null|member $member
494
	 *
495
	 * @return bool
496
	 */
497
	public function can($perm, $member = null) {
498
		if(!$member) $member = Member::currentUser();
499
		if(is_array($perm)) {
500
			$perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member));
501
			return min($perm);
502
		}
503
		if($this->hasMethod($methodName = 'can' . $perm)) {
504
			return $this->$methodName($member);
505
		} else {
506
			return true;
507
		}
508
	}
509
510
	/**
511
	 * Pushes this controller onto the stack of current controllers. This means that any redirection,
512
	 * session setting, or other things that rely on Controller::curr() will now write to this
513
	 * controller object.
514
	 */
515
	public function pushCurrent() {
516
		array_unshift(self::$controller_stack, $this);
517
		// Create a new session object
518
		if(!$this->session) {
519
			if(isset(self::$controller_stack[1])) {
520
				$this->session = self::$controller_stack[1]->getSession();
521
			} else {
522
				$this->session = Injector::inst()->create('Session', array());
523
			}
524
		}
525
	}
526
527
	/**
528
	 * Pop this controller off the top of the stack.
529
	 */
530
	public function popCurrent() {
531
		if($this === self::$controller_stack[0]) {
532
			array_shift(self::$controller_stack);
533
		} else {
534
			user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack",
535
				E_USER_WARNING);
536
		}
537
	}
538
539
	/**
540
	 * Redirect to the given URL.
541
	 *
542
	 * @param string $url
543
	 * @param int $code
544
	 *
545
	 * @return SS_HTTPResponse
546
	 */
547
	public function redirect($url, $code=302) {
548
549
		if($this->getResponse()->getHeader('Location') && $this->getResponse()->getHeader('Location') != $url) {
550
			user_error("Already directed to " . $this->getResponse()->getHeader('Location')
551
				. "; now trying to direct to $url", E_USER_WARNING);
552
			return;
553
		}
554
555
		// Attach site-root to relative links, if they have a slash in them
556
		if($url=="" || $url[0]=='?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)) {
557
			$url = Director::baseURL() . $url;
558
		}
559
560
		return $this->getResponse()->redirect($url, $code);
561
	}
562
563
	/**
564
	 * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
565
	 * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
566
	 * location.href in IE). If none of the two variables is available, it will redirect to the base
567
	 * URL (see {@link Director::baseURL()}).
568
	 *
569
	 * @uses redirect()
570
	 *
571
	 * @return bool|SS_HTTPResponse
572
	 */
573
	public function redirectBack() {
574
		// Don't cache the redirect back ever
575
		HTTP::set_cache_age(0);
576
577
		$url = null;
578
579
		// In edge-cases, this will be called outside of a handleRequest() context; in that case,
580
		// redirect to the homepage - don't break into the global state at this stage because we'll
581
		// be calling from a test context or something else where the global state is inappropraite
582
		if($this->getRequest()) {
583
			if($this->getRequest()->requestVar('BackURL')) {
584
				$url = $this->getRequest()->requestVar('BackURL');
585
			} else if($this->getRequest()->isAjax() && $this->getRequest()->getHeader('X-Backurl')) {
586
				$url = $this->getRequest()->getHeader('X-Backurl');
587
			} else if($this->getRequest()->getHeader('Referer')) {
588
				$url = $this->getRequest()->getHeader('Referer');
589
			}
590
		}
591
592
		if(!$url) $url = Director::baseURL();
593
594
		// absolute redirection URLs not located on this site may cause phishing
595
		if(Director::is_site_url($url)) {
596
			$url = Director::absoluteURL($url, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, 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...
597
			return $this->redirect($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \Director::absoluteURL($url, true) on line 596 can also be of type false; however, Controller::redirect() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
598
		} else {
599
			return false;
600
		}
601
602
	}
603
604
	/**
605
	 * Tests whether a redirection has been requested. If redirect() has been called, it will return
606
	 * the URL redirected to. Otherwise, it will return null.
607
	 *
608
	 * @return null|string
609
	 */
610
	public function redirectedTo() {
611
		return $this->getResponse() && $this->getResponse()->getHeader('Location');
612
	}
613
614
	/**
615
	 * Get the Session object representing this Controller's session.
616
	 *
617
	 * @return Session
618
	 */
619
	public function getSession() {
620
		return $this->session;
621
	}
622
623
	/**
624
	 * Set the Session object.
625
	 *
626
	 * @param Session $session
627
	 */
628
	public function setSession(Session $session) {
629
		$this->session = $session;
630
	}
631
632
	/**
633
	 * Joins two or more link segments together, putting a slash between them if necessary. Use this
634
	 * for building the results of {@link Link()} methods. If either of the links have query strings,
635
	 * then they will be combined and put at the end of the resulting url.
636
	 *
637
	 * Caution: All parameters are expected to be URI-encoded already.
638
	 *
639
	 * @param string
640
	 *
641
	 * @return string
642
	 */
643
	public static function join_links() {
644
		$args = func_get_args();
645
		$result = "";
646
		$queryargs = array();
647
		$fragmentIdentifier = null;
648
649
		foreach($args as $arg) {
650
			// Find fragment identifier - keep the last one
651
			if(strpos($arg,'#') !== false) {
652
				list($arg, $fragmentIdentifier) = explode('#',$arg,2);
653
			}
654
			// Find querystrings
655
			if(strpos($arg,'?') !== false) {
656
				list($arg, $suffix) = explode('?',$arg,2);
657
				parse_str($suffix, $localargs);
658
				$queryargs = array_merge($queryargs, $localargs);
659
			}
660
			if((is_string($arg) && $arg) || is_numeric($arg)) {
661
				$arg = (string) $arg;
662
				if($result && substr($result,-1) != '/' && $arg[0] != '/') $result .= "/$arg";
663
				else $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
664
			}
665
		}
666
667
		if($queryargs) $result .= '?' . http_build_query($queryargs);
0 ignored issues
show
Bug Best Practice introduced by
The expression $queryargs 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...
668
669
		if($fragmentIdentifier) $result .= "#$fragmentIdentifier";
670
671
		return $result;
672
	}
673
674
	/**
675
	 * @return array
676
	 */
677
	public static function get_template_global_variables() {
678
		return array(
679
			'CurrentPage' => 'curr',
680
		);
681
	}
682
}
683
684
685