Completed
Push — namespace-model ( 476d0e...5d15d8 )
by Sam
08:12
created

Controller   D

Complexity

Total Complexity 104

Size/Duplication

Total Lines 670
Duplicated Lines 2.39 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

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

30 Methods

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
203
204
		$this->popCurrent();
205
		return $response;
206
	}
207
208
	/**
209
	 * Controller's default action handler.  It will call the method named in "$Action", if that method
210
	 * exists. If "$Action" isn't given, it will use "index" as a default.
211
	 *
212
	 * @param SS_HTTPRequest $request
213
	 * @param string $action
214
	 *
215
	 * @return HTMLText|SS_HTTPResponse
216
	 */
217
	protected function handleAction($request, $action) {
218
		foreach($request->latestParams() as $k => $v) {
219
			if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
220
		}
221
222
		$this->action = $action;
223
		$this->requestParams = $request->requestVars();
224
225
		if($this->hasMethod($action)) {
226
			$result = parent::handleAction($request, $action);
227
228
			// If the action returns an array, customise with it before rendering the template.
229
			if(is_array($result)) {
230
				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 SilverStripe\Control\Controller::handleAction of type SilverStripe\Control\HTM...Control\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...
231
			} else {
232
				return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (SilverStripe\Control\HTTPResponse) is incompatible with the return type documented by SilverStripe\Control\Controller::handleAction of type SilverStripe\Control\HTM...Control\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...
233
			}
234
		}
235
236
		// Fall back to index action with before/after handlers
237
		$beforeResult = $this->extend('beforeCallActionHandler', $request, $action);
238
		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...
239
			return reset($beforeResult);
240
		}
241
242
		$result = $this->getViewer($action)->process($this);
243
244
		$afterResult = $this->extend('afterCallActionHandler', $request, $action, $result);
245
		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...
246
			return reset($afterResult);
247
		}
248
249
		return $result;
250
	}
251
252
	/**
253
	 * @param array $urlParams
254
	 */
255
	public function setURLParams($urlParams) {
256
		$this->urlParams = $urlParams;
257
	}
258
259
	/**
260
	 * Returns the parameters extracted from the URL by the {@link Director}.
261
	 *
262
	 * @return array
263
	 */
264
	public function getURLParams() {
265
		return $this->urlParams;
266
	}
267
268
	/**
269
	 * Returns the SS_HTTPResponse object that this controller is building up. Can be used to set the
270
	 * status code and headers.
271
	 *
272
	 * @return HTTPResponse
273
	 */
274
	public function getResponse() {
275
		if (!$this->response) {
276
			$this->setResponse(new HTTPResponse());
277
		}
278
		return $this->response;
279
	}
280
281
	/**
282
	 * Sets the SS_HTTPResponse object that this controller is building up.
283
	 *
284
	 * @param SS_HTTPResponse $response
285
	 *
286
	 * @return $this
287
	 */
288
	public function setResponse(HTTPResponse $response) {
289
		$this->response = $response;
0 ignored issues
show
Documentation Bug introduced by
It seems like $response of type object<SilverStripe\Control\HTTPResponse> is incompatible with the declared type object<SilverStripe\Control\SS_HTTPResponse> of property $response.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

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

Loading history...
290
		return $this;
291
	}
292
293
	/**
294
	 * @var bool
295
	 */
296
	protected $baseInitCalled = false;
297
298
	/**
299
	 * Return the object that is going to own a form that's being processed, and handle its execution.
300
	 * Note that the result need not be an actual controller object.
301
	 *
302
	 * @return mixed
303
	 */
304
	public function getFormOwner() {
305
		// Get the appropriate controller: sometimes we want to get a form from another controller
306
		if(isset($this->requestParams['formController'])) {
307
			$formController = Director::getControllerForURL($this->requestParams['formController']);
308
309
			while(is_a($formController, 'NestedController')) {
310
				$formController = $formController->getNestedController();
311
			}
312
			return $formController;
313
314
		} else {
315
			return $this;
316
		}
317
	}
318
319
	/**
320
	 * This is the default action handler used if a method doesn't exist. It will process the
321
	 * controller object with the template returned by {@link getViewer()}.
322
	 *
323
	 * @param string $action
324
	 *
325
	 * @return HTMLText
326
	 */
327
	public function defaultAction($action) {
328
		return $this->getViewer($action)->process($this);
329
	}
330
331
	/**
332
	 * Returns the action that is being executed on this controller.
333
	 *
334
	 * @return string
335
	 */
336
	public function getAction() {
337
		return $this->action;
338
	}
339
340
	/**
341
	 * Return the viewer identified being the default handler for this Controller/Action combination.
342
	 *
343
	 * @param string $action
344
	 *
345
	 * @return SSViewer
346
	 */
347
	public function getViewer($action) {
348
		// Hard-coded templates
349
		if(isset($this->templates[$action]) && $this->templates[$action]) {
350
			$templates = $this->templates[$action];
351
352
		}	else if(isset($this->templates['index']) && $this->templates['index']) {
353
			$templates = $this->templates['index'];
354
355
		}	else if($this->template) {
356
			$templates = $this->template;
0 ignored issues
show
Documentation introduced by
The property template does not exist on object<SilverStripe\Control\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...
357
		} else {
358
			// Add action-specific templates for inheritance chain
359
			$templates = array();
360
			$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...
361
			if($action && $action != 'index') {
362
				$parentClass = $this->class;
363 View Code Duplication
				while($parentClass != 'SilverStripe\Control\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...
364
					$templates[] = strtok($parentClass,'_') . '_' . $action;
365
					$parentClass = get_parent_class($parentClass);
366
				}
367
			}
368
			// Add controller templates for inheritance chain
369
			$parentClass = $this->class;
370 View Code Duplication
			while($parentClass != 'SilverStripe\Control\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...
371
				$templates[] = strtok($parentClass,'_');
372
				$parentClass = get_parent_class($parentClass);
373
			}
374
375
			$templates[] = 'SilverStripe\Control\Controller';
376
377
			// remove duplicates
378
			$templates = array_unique($templates);
379
		}
380
381
		return new SSViewer($templates);
382
	}
383
384
	/**
385
	 * @param string $action
386
	 *
387
	 * @return bool
388
	 */
389
	public function hasAction($action) {
390
		return parent::hasAction($action) || $this->hasActionTemplate($action);
391
	}
392
393
	/**
394
	 * Removes all the "action" part of the current URL and returns the result. If no action parameter
395
	 * is present, returns the full URL.
396
	 *
397
	 * @param string $fullURL
398
	 * @param null|string $action
399
	 *
400
	 * @return string
401
	 */
402
	public function removeAction($fullURL, $action = null) {
403
		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...
404
		$returnURL = $fullURL;
405
406
		if (($pos = strpos($fullURL, $action)) !== false) {
407
			$returnURL = substr($fullURL,0,$pos);
408
		}
409
410
		return $returnURL;
411
	}
412
413
	/**
414
	 * Return the class that defines the given action, so that we know where to check allowed_actions.
415
	 * Overrides RequestHandler to also look at defined templates.
416
	 *
417
	 * @param string $action
418
	 *
419
	 * @return string
420
	 */
421
	protected function definingClassForAction($action) {
422
		$definingClass = parent::definingClassForAction($action);
423
		if($definingClass) return $definingClass;
424
425
		$class = get_class($this);
426
		while($class != 'SilverStripe\Control\RequestHandler') {
427
			$templateName = strtok($class, '_') . '_' . $action;
428
			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...
429
430
			$class = get_parent_class($class);
431
		}
432
	}
433
434
	/**
435
	 * Returns TRUE if this controller has a template that is specifically designed to handle a
436
	 * specific action.
437
	 *
438
	 * @param string $action
439
	 *
440
	 * @return bool
441
	 */
442
	public function hasActionTemplate($action) {
443
		if(isset($this->templates[$action])) return true;
444
445
		$parentClass = $this->class;
446
		$templates   = array();
447
448 View Code Duplication
		while($parentClass != 'SilverStripe\Control\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...
449
			$templates[] = strtok($parentClass, '_') . '_' . $action;
450
			$parentClass = get_parent_class($parentClass);
451
		}
452
453
		return SSViewer::hasTemplate($templates);
454
	}
455
456
	/**
457
	 * Render the current controller with the templates determined by {@link getViewer()}.
458
	 *
459
	 * @param array $params
460
	 *
461
	 * @return string
462
	 */
463
	public function render($params = null) {
464
		$template = $this->getViewer($this->getAction());
465
466
		// if the object is already customised (e.g. through Controller->run()), use it
467
		$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...
468
469
		if($params) $obj = $this->customise($params);
470
471
		return $template->process($obj);
472
	}
473
474
	/**
475
	 * Call this to disable site-wide basic authentication for a specific controller. This must be
476
	 * called before Controller::init(). That is, you must call it in your controller's init method
477
	 * before it calls parent::init().
478
	 */
479
	public function disableBasicAuth() {
480
		$this->basicAuthEnabled = false;
481
	}
482
483
	/**
484
	 * Returns the current controller.
485
	 *
486
	 * @return Controller
487
	 */
488
	public static function curr() {
489
		if(Controller::$controller_stack) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\Control\Co...ller::$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...
490
			return Controller::$controller_stack[0];
491
		} else {
492
			user_error("No current controller available", E_USER_WARNING);
493
		}
494
	}
495
496
	/**
497
	 * Tests whether we have a currently active controller or not. True if there is at least 1
498
	 * controller in the stack.
499
	 *
500
	 * @return bool
501
	 */
502
	public static function has_curr() {
503
		return Controller::$controller_stack ? true : false;
504
	}
505
506
	/**
507
	 * Returns true if the member is allowed to do the given action. Defaults to the currently logged
508
	 * in user.
509
	 *
510
	 * @param string $perm
511
	 * @param null|member $member
512
	 *
513
	 * @return bool
514
	 */
515
	public function can($perm, $member = null) {
516
		if(!$member) $member = Member::currentUser();
517
		if(is_array($perm)) {
518
			$perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member));
519
			return min($perm);
520
		}
521
		if($this->hasMethod($methodName = 'can' . $perm)) {
522
			return $this->$methodName($member);
523
		} else {
524
			return true;
525
		}
526
	}
527
528
	/**
529
	 * Pushes this controller onto the stack of current controllers. This means that any redirection,
530
	 * session setting, or other things that rely on Controller::curr() will now write to this
531
	 * controller object.
532
	 */
533
	public function pushCurrent() {
534
		array_unshift(self::$controller_stack, $this);
535
		// Create a new session object
536
		if(!$this->session) {
537
			if(isset(self::$controller_stack[1])) {
538
				$this->session = self::$controller_stack[1]->getSession();
539
			} else {
540
				$this->session = Injector::inst()->create('SilverStripe\Control\Session', array());
541
			}
542
		}
543
	}
544
545
	/**
546
	 * Pop this controller off the top of the stack.
547
	 */
548
	public function popCurrent() {
549
		if($this === self::$controller_stack[0]) {
550
			array_shift(self::$controller_stack);
551
		} else {
552
			user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack",
553
				E_USER_WARNING);
554
		}
555
	}
556
557
	/**
558
	 * Redirect to the given URL.
559
	 *
560
	 * @param string $url
561
	 * @param int $code
562
	 *
563
	 * @return HTTPResponse
564
	 */
565
	public function redirect($url, $code=302) {
566
567
		if($this->getResponse()->getHeader('Location') && $this->getResponse()->getHeader('Location') != $url) {
568
			user_error("Already directed to " . $this->getResponse()->getHeader('Location')
569
				. "; now trying to direct to $url", E_USER_WARNING);
570
			return;
571
		}
572
573
		// Attach site-root to relative links, if they have a slash in them
574
		if($url=="" || $url[0]=='?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)) {
575
			$url = Director::baseURL() . $url;
576
		}
577
578
		return $this->getResponse()->redirect($url, $code);
579
	}
580
581
	/**
582
	 * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
583
	 * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
584
	 * location.href in IE). If none of the two variables is available, it will redirect to the base
585
	 * URL (see {@link Director::baseURL()}).
586
	 *
587
	 * @uses redirect()
588
	 *
589
	 * @return bool|SS_HTTPResponse
590
	 */
591
	public function redirectBack() {
592
		// Don't cache the redirect back ever
593
		HTTP::set_cache_age(0);
594
595
		$url = null;
596
597
		// In edge-cases, this will be called outside of a handleRequest() context; in that case,
598
		// redirect to the homepage - don't break into the global state at this stage because we'll
599
		// be calling from a test context or something else where the global state is inappropraite
600
		if($this->getRequest()) {
601
			if($this->getRequest()->requestVar('BackURL')) {
602
				$url = $this->getRequest()->requestVar('BackURL');
603
			} else if($this->getRequest()->isAjax() && $this->getRequest()->getHeader('X-Backurl')) {
604
				$url = $this->getRequest()->getHeader('X-Backurl');
605
			} else if($this->getRequest()->getHeader('Referer')) {
606
				$url = $this->getRequest()->getHeader('Referer');
607
			}
608
		}
609
610
		if(!$url) $url = Director::baseURL();
611
612
		// absolute redirection URLs not located on this site may cause phishing
613
		if(Director::is_site_url($url)) {
614
			$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...
615
			return $this->redirect($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Di...absoluteURL($url, true) on line 614 can also be of type false; however, SilverStripe\Control\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...
616
		} else {
617
			return false;
618
		}
619
620
	}
621
622
	/**
623
	 * Tests whether a redirection has been requested. If redirect() has been called, it will return
624
	 * the URL redirected to. Otherwise, it will return null.
625
	 *
626
	 * @return null|string
627
	 */
628
	public function redirectedTo() {
629
		return $this->getResponse() && $this->getResponse()->getHeader('Location');
630
	}
631
632
	/**
633
	 * Get the Session object representing this Controller's session.
634
	 *
635
	 * @return Session
636
	 */
637
	public function getSession() {
638
		return $this->session;
639
	}
640
641
	/**
642
	 * Set the Session object.
643
	 *
644
	 * @param Session $session
645
	 */
646
	public function setSession(Session $session) {
647
		$this->session = $session;
648
	}
649
650
	/**
651
	 * Joins two or more link segments together, putting a slash between them if necessary. Use this
652
	 * for building the results of {@link Link()} methods. If either of the links have query strings,
653
	 * then they will be combined and put at the end of the resulting url.
654
	 *
655
	 * Caution: All parameters are expected to be URI-encoded already.
656
	 *
657
	 * @param string
658
	 *
659
	 * @return string
660
	 */
661
	public static function join_links() {
662
		$args = func_get_args();
663
		$result = "";
664
		$queryargs = array();
665
		$fragmentIdentifier = null;
666
667
		foreach($args as $arg) {
668
			// Find fragment identifier - keep the last one
669
			if(strpos($arg,'#') !== false) {
670
				list($arg, $fragmentIdentifier) = explode('#',$arg,2);
671
			}
672
			// Find querystrings
673
			if(strpos($arg,'?') !== false) {
674
				list($arg, $suffix) = explode('?',$arg,2);
675
				parse_str($suffix, $localargs);
676
				$queryargs = array_merge($queryargs, $localargs);
677
			}
678
			if((is_string($arg) && $arg) || is_numeric($arg)) {
679
				$arg = (string) $arg;
680
				if($result && substr($result,-1) != '/' && $arg[0] != '/') $result .= "/$arg";
681
				else $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
682
			}
683
		}
684
685
		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...
686
687
		if($fragmentIdentifier) $result .= "#$fragmentIdentifier";
688
689
		return $result;
690
	}
691
692
	/**
693
	 * @return array
694
	 */
695
	public static function get_template_global_variables() {
696
		return array(
697
			'CurrentPage' => 'curr',
698
		);
699
	}
700
}
701
702
703