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

RequestHandler   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 465
Duplicated Lines 1.29 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 2
Bugs 2 Features 0
Metric Value
wmc 83
c 2
b 2
f 0
lcom 1
cbo 15
dl 6
loc 465
rs 1.5789

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A setDataModel() 0 3 1
F handleRequest() 3 90 24
D findAction() 3 27 9
B handleAction() 0 17 5
B allowedActions() 0 33 6
C hasAction() 0 34 15
A definingClassForAction() 0 12 3
C checkAccessAction() 0 50 15
A httpError() 0 13 1
A getRequest() 0 3 1
A setRequest() 0 3 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 RequestHandler 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 RequestHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Control;
4
5
/**
6
 * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
7
 *
8
 * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
9
 * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
10
 * controller is being used.  It will then call {@link handleRequest()} on that Controller, passing it the parameters
11
 * that it parsed from the URL, and the {@link SS_HTTPRequest} that contains the remainder of the URL to be parsed.
12
 *
13
 * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
14
 *
15
 * In SilverStripe, URL parsing is distributed throughout the object graph.  For example, suppose that we have a
16
 * search form that contains a {@link TreeMultiSelectField} named "Groups".  We want to use ajax to load segments of
17
 * this tree as they are needed rather than downloading the tree right at the beginning.  We could use this URL to get
18
 * the tree segment that appears underneath
19
 *
20
 * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
21
 *  - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
22
 *    Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
23
 *  - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
24
 *    pass control to that.
25
 *    Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
26
 *  - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
27
 *    control to that.
28
 *    Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
29
 *  - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method.  This method
30
 *    will return an HTML fragment that is output to the screen.
31
 *    Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
32
 *
33
 * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
34
 *
35
 * @package framework
36
 * @subpackage control
37
 */
38
39
use SilverStripe\Model\DataModel;
40
41
use Debug;
42
43
use Security;
44
use PermissionFailureException;
45
46
use Config;
47
use Deprecation;
48
use ReflectionClass;
49
use Object;
50
use Permission;
51
use ViewableData;
52
use SilverStripe\Control\HTTPRequest;
53
use SilverStripe\Control\HTTPResponse_Exception;
54
use SilverStripe\Control\HTTPResponse;
55
56
57
class RequestHandler extends ViewableData {
58
59
	/**
60
	 * @var SS_HTTPRequest $request The request object that the controller was called with.
61
	 * Set in {@link handleRequest()}. Useful to generate the {}
62
	 */
63
	protected $request = null;
64
65
	/**
66
	 * The DataModel for this request
67
	 */
68
	protected $model = null;
69
70
	/**
71
	 * This variable records whether RequestHandler::__construct()
72
	 * was called or not. Useful for checking if subclasses have
73
	 * called parent::__construct()
74
	 *
75
	 * @var boolean
76
	 */
77
	protected $brokenOnConstruct = true;
78
79
	/**
80
	 * The default URL handling rules.  This specifies that the next component of the URL corresponds to a method to
81
	 * be called on this RequestHandlingData object.
82
	 *
83
	 * The keys of this array are parse rules.  See {@link SS_HTTPRequest::match()} for a description of the rules
84
	 * available.
85
	 *
86
	 * The values of the array are the method to be called if the rule matches.  If this value starts with a '$', then
87
	 * the named parameter of the parsed URL wil be used to determine the method name.
88
	 * @config
89
	 */
90
	private static $url_handlers = array(
91
		'$Action' => '$Action',
92
	);
93
94
95
	/**
96
	 * Define a list of action handling methods that are allowed to be called directly by URLs.
97
	 * The variable should be an array of action names. This sample shows the different values that it can contain:
98
	 *
99
	 * <code>
100
	 * array(
101
	 * 		// someaction can be accessed by anyone, any time
102
	 *		'someaction',
103
	 *		// So can otheraction
104
	 *		'otheraction' => true,
105
	 *		// restrictedaction can only be people with ADMIN privilege
106
	 *		'restrictedaction' => 'ADMIN',
107
	 *		// complexaction can only be accessed if $this->canComplexAction() returns true
108
	 *		'complexaction' '->canComplexAction'
109
	 *	);
110
	 * </code>
111
	 *
112
	 * Form getters count as URL actions as well, and should be included in allowed_actions.
113
	 * Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
114
	 * these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
115
	 * either by conditionally removing {@link FormAction} in the form construction,
116
	 * or by defining $allowed_actions in your {@link Form} class.
117
	 * @config
118
	 */
119
	private static $allowed_actions = null;
120
121
	/**
122
	 * @config
123
	 * @var boolean Enforce presence of $allowed_actions when checking acccess.
124
	 * Defaults to TRUE, meaning all URL actions will be denied.
125
	 * When set to FALSE, the controller will allow *all* public methods to be called.
126
	 * In most cases this isn't desireable, and in fact a security risk,
127
	 * since some helper methods can cause side effects which shouldn't be exposed through URLs.
128
	 */
129
	private static $require_allowed_actions = true;
130
131
	public function __construct() {
132
		$this->brokenOnConstruct = false;
133
134
		// Check necessary to avoid class conflicts before manifest is rebuilt
135
		if(class_exists('SilverStripe\Control\NullHTTPRequest')) $this->setRequest(new NullHTTPRequest());
136
137
		// This will prevent bugs if setDataModel() isn't called.
138
		$this->model = DataModel::inst();
139
140
		parent::__construct();
141
	}
142
143
	/**
144
	 * Set the DataModel for this request.
145
	 */
146
	public function setDataModel($model) {
147
		$this->model = $model;
148
	}
149
150
	/**
151
	 * Handles URL requests.
152
	 *
153
	 *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
154
	 *  - If the rule matches, the named method will be called.
155
	 *  - If there is still more URL to be processed, then handleRequest()
156
	 *    is called on the object that that method returns.
157
	 *
158
	 * Once all of the URL has been processed, the final result is returned.
159
	 * However, if the final result is an array, this
160
	 * array is interpreted as being additional template data to customise the
161
	 * 2nd to last result with, rather than an object
162
	 * in its own right.  This is most frequently used when a Controller's
163
	 * action will return an array of data with which to
164
	 * customise the controller.
165
	 *
166
	 * @param $request The {@link SS_HTTPRequest} object that is reponsible for distributing URL parsing
167
	 * @uses SS_HTTPRequest
168
	 * @uses SS_HTTPRequest->match()
169
	 * @return HTTPResponse|RequestHandler|string|array
170
	 */
171
	public function handleRequest(HTTPRequest $request, DataModel $model) {
172
		// $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
173
		$handlerClass = ($this->class) ? $this->class : get_class($this);
174
175
		if($this->brokenOnConstruct) {
176
			user_error("parent::__construct() needs to be called on {$handlerClass}::__construct()", E_USER_WARNING);
177
		}
178
179
		$this->setRequest($request);
180
		$this->setDataModel($model);
181
182
		$match = $this->findAction($request);
183
184
		// If nothing matches, return this object
185
		if (!$match) return $this;
186
187
		// Start to find what action to call. Start by using what findAction returned
188
		$action = $match['action'];
189
190
		// We used to put "handleAction" as the action on controllers, but (a) this could only be called when
191
		// you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
192
		if ($action == 'handleAction') {
193
			// TODO Fix LeftAndMain usage
194
			// Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
195
			$action = '$Action';
196
		}
197
198
		// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
199
		if($action[0] == '$') {
200
			$action = str_replace("-", "_", $request->latestParam(substr($action,1)));
201
		}
202
203
		if(!$action) {
204
			if(isset($_REQUEST['debug_request'])) {
205
				Debug::message("Action not set; using default action method name 'index'");
206
			}
207
			$action = "index";
208 View Code Duplication
		} else if(!is_string($action)) {
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...
209
			user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
210
		}
211
212
		$classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
213
214
		try {
215
			if(!$this->hasAction($action)) {
216
				return $this->httpError(404, "Action '$action' isn't available $classMessage.");
217
			}
218
			if(!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'init'))) {
219
				return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
220
			}
221
			$result = $this->handleAction($request, $action);
222
		}
223
		catch (HTTPResponse_Exception $e) {
224
			return $e->getResponse();
225
		}
226
		catch(PermissionFailureException $e) {
227
			$result = Security::permissionFailure(null, $e->getMessage());
228
		}
229
230
		if($result instanceof HTTPResponse && $result->isError()) {
231
			if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
232
			return $result;
233
		}
234
235
		// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
236
		// parse. It might have its own handler. However, we only do this if we haven't just parsed an
237
		// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
238
		// actions which return themselves to avoid infinite loops.
239
		$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
240
		$resultIsRequestHandler = is_object($result) && $result instanceof RequestHandler;
241
242
		if($this !== $result && !$matchedRuleWasEmpty && $resultIsRequestHandler) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matchedRuleWasEmpty of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
243
			$returnValue = $result->handleRequest($request, $model);
244
245
			// Array results can be used to handle
246
			if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
247
248
			return $returnValue;
249
250
		// If we return some other data, and all the URL is parsed, then return that
251
		} else if($request->allParsed()) {
252
			return $result;
253
254
		// But if we have more content on the URL and we don't know what to do with it, return an error.
255
		} else {
256
			return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
257
		}
258
259
		return $this;
0 ignored issues
show
Unused Code introduced by
return $this; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
260
	}
261
262
	protected function findAction($request) {
263
		$handlerClass = ($this->class) ? $this->class : get_class($this);
264
265
		// We stop after RequestHandler; in other words, at ViewableData
266
		while($handlerClass && $handlerClass != 'ViewableData') {
267
			$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
268
269
			if($urlHandlers) foreach($urlHandlers as $rule => $action) {
0 ignored issues
show
Bug introduced by
The expression $urlHandlers of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
270 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...
271
					Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
272
				}
273
274
				if($request->match($rule, true)) {
275
					if(isset($_REQUEST['debug_request'])) {
276
						Debug::message(
277
							"Rule '$rule' matched to action '$action' on $this->class. ".
278
							"Latest request params: " . var_export($request->latestParams(), true)
279
						);
280
					}
281
282
					return array('rule' => $rule, 'action' => $action);
283
				}
284
			}
285
286
			$handlerClass = get_parent_class($handlerClass);
287
		}
288
	}
289
290
	/**
291
	 * Given a request, and an action name, call that action name on this RequestHandler
292
	 *
293
	 * Must not raise SS_HTTPResponse_Exceptions - instead it should return
294
	 *
295
	 * @param $request
296
	 * @param $action
297
	 * @return HTTPResponse
298
	 */
299
	protected function handleAction($request, $action) {
300
		$classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
301
302
		if(!$this->hasMethod($action)) {
303
			return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
304
		}
305
306
		$res = $this->extend('beforeCallActionHandler', $request, $action);
307
		if ($res) return reset($res);
0 ignored issues
show
Bug Best Practice introduced by
The expression $res 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...
308
309
		$actionRes = $this->$action($request);
310
311
		$res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
312
		if ($res) return reset($res);
0 ignored issues
show
Bug Best Practice introduced by
The expression $res 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...
313
314
		return $actionRes;
315
	}
316
317
	/**
318
	 * Get a array of allowed actions defined on this controller,
319
	 * any parent classes or extensions.
320
	 *
321
	 * Caution: Since 3.1, allowed_actions definitions only apply
322
	 * to methods on the controller they're defined on,
323
	 * so it is recommended to use the $class argument
324
	 * when invoking this method.
325
	 *
326
	 * @param string $limitToClass
327
	 * @return array|null
328
	 */
329
	public function allowedActions($limitToClass = null) {
330
		if($limitToClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limitToClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
331
			$actions = Config::inst()->get(
332
				$limitToClass,
333
				'allowed_actions',
334
				Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
335
			);
336
		} else {
337
			$actions = Config::inst()->get(get_class($this), 'allowed_actions');
338
		}
339
340
		if(is_array($actions)) {
341
			if(array_key_exists('*', $actions)) {
342
				Deprecation::notice(
343
					'3.0',
344
					'Wildcards (*) are no longer valid in $allowed_actions due their ambiguous '
345
					. ' and potentially insecure behaviour. Please define all methods explicitly instead.'
346
				);
347
			}
348
349
			// convert all keys and values to lowercase to
350
			// allow for easier comparison, unless it is a permission code
351
			$actions = array_change_key_case($actions, CASE_LOWER);
352
353
			foreach($actions as $key => $value) {
354
				if(is_numeric($key)) $actions[$key] = strtolower($value);
355
			}
356
357
			return $actions;
358
		} else {
359
			return null;
360
		}
361
	}
362
363
	/**
364
	 * Checks if this request handler has a specific action,
365
	 * even if the current user cannot access it.
366
	 * Includes class ancestry and extensions in the checks.
367
	 *
368
	 * @param string $action
369
	 * @return bool
370
	 */
371
	public function hasAction($action) {
372
		if($action == 'index') return true;
373
374
		// Don't allow access to any non-public methods (inspect instance plus all extensions)
375
		$insts = array_merge(array($this), (array) $this->getExtensionInstances());
376
		foreach($insts as $inst) {
377
			if(!method_exists($inst, $action)) continue;
378
			$r = new ReflectionClass(get_class($inst));
379
			$m = $r->getMethod($action);
380
			if(!$m || !$m->isPublic()) return false;
381
		}
382
383
		$action  = strtolower($action);
384
		$actions = $this->allowedActions();
385
386
		// Check if the action is defined in the allowed actions of any ancestry class
387
		// as either a key or value. Note that if the action is numeric, then keys are not
388
		// searched for actions to prevent actual array keys being recognised as actions.
389
		if(is_array($actions)) {
390
			$isKey   = !is_numeric($action) && array_key_exists($action, $actions);
391
			$isValue = in_array($action, $actions, true);
392
			if($isKey || $isValue) return true;
393
		}
394
395
		$actionsWithoutExtra = $this->config()->get(
396
			'allowed_actions',
397
			Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
398
		);
399
		if(!is_array($actions) || !$actionsWithoutExtra) {
400
			if($action != 'init' && $action != 'run' && method_exists($this, $action)) return true;
401
		}
402
403
		return false;
404
	}
405
406
	/**
407
	 * Return the class that defines the given action, so that we know where to check allowed_actions.
408
	 */
409
	protected function definingClassForAction($actionOrigCasing) {
410
		$action = strtolower($actionOrigCasing);
411
412
		$definingClass = null;
0 ignored issues
show
Unused Code introduced by
$definingClass 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...
413
		$insts = array_merge(array($this), (array) $this->getExtensionInstances());
414
		foreach($insts as $inst) {
415
			if(!method_exists($inst, $action)) continue;
416
			$r = new ReflectionClass(get_class($inst));
417
			$m = $r->getMethod($actionOrigCasing);
418
			return $m->getDeclaringClass()->getName();
419
		}
420
	}
421
422
	/**
423
	 * Check that the given action is allowed to be called from a URL.
424
	 * It will interrogate {@link self::$allowed_actions} to determine this.
425
	 */
426
	public function checkAccessAction($action) {
427
		$actionOrigCasing = $action;
428
		$action = strtolower($action);
429
430
		$isAllowed = false;
431
		$isDefined = false;
432
433
		// Get actions for this specific class (without inheritance)
434
		$definingClass = $this->definingClassForAction($actionOrigCasing);
435
		$allowedActions = $this->allowedActions($definingClass);
436
437
		// check if specific action is set
438
		if(isset($allowedActions[$action])) {
439
			$isDefined = true;
440
			$test = $allowedActions[$action];
441
			if($test === true || $test === 1 || $test === '1') {
442
				// TRUE should always allow access
443
				$isAllowed = true;
444
			} elseif(substr($test, 0, 2) == '->') {
445
				// Determined by custom method with "->" prefix
446
				list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
447
				$isAllowed = call_user_func_array(array($this, $method), $arguments);
448
			} else {
449
				// Value is a permission code to check the current member against
450
				$isAllowed = Permission::check($test);
451
			}
452
		} elseif(
453
			is_array($allowedActions)
454
			&& (($key = array_search($action, $allowedActions, true)) !== false)
455
			&& is_numeric($key)
456
		) {
457
			// Allow numeric array notation (search for array value as action instead of key)
458
			$isDefined = true;
459
			$isAllowed = true;
460
		} elseif(is_array($allowedActions) && !count($allowedActions)) {
461
			// If defined as empty array, deny action
462
			$isAllowed = false;
463
		} elseif($allowedActions === null) {
464
			// If undefined, allow action based on configuration
465
			$isAllowed = !Config::inst()->get('SilverStripe\Control\RequestHandler', 'require_allowed_actions');
466
		}
467
468
		// If we don't have a match in allowed_actions,
469
		// whitelist the 'index' action as well as undefined actions based on configuration.
470
		if(!$isDefined && ($action == 'index' || empty($action))) {
471
			$isAllowed = true;
472
		}
473
474
		return $isAllowed;
475
	}
476
477
	/**
478
	 * Throws a HTTP error response encased in a {@link SS_HTTPResponse_Exception}, which is later caught in
479
	 * {@link RequestHandler::handleAction()} and returned to the user.
480
	 *
481
	 * @param int $errorCode
482
	 * @param string $errorMessage Plaintext error message
483
	 * @uses SilverStripe\Control\HTTPResponse_Exception
484
	 * @throws SilverStripe\Control\HTTPResponse_Exception
485
	 */
486
	public function httpError($errorCode, $errorMessage = null) {
487
488
		$request = $this->getRequest();
489
490
		// Call a handler method such as onBeforeHTTPError404
491
		$this->extend('onBeforeHTTPError' . $errorCode, $request);
492
493
		// Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
494
		$this->extend('onBeforeHTTPError', $errorCode, $request);
495
496
		// Throw a new exception
497
		throw new HTTPResponse_Exception($errorMessage, $errorCode);
498
	}
499
500
	/**
501
	 * Returns the SS_HTTPRequest object that this controller is using.
502
	 * Returns a placeholder {@link NullHTTPRequest} object unless
503
	 * {@link handleAction()} or {@link handleRequest()} have been called,
504
	 * which adds a reference to an actual {@link SS_HTTPRequest} object.
505
	 *
506
	 * @return SS_HTTPRequest|NullHTTPRequest
507
	 */
508
	public function getRequest() {
509
		return $this->request;
510
	}
511
512
	/**
513
	 * Typically the request is set through {@link handleAction()}
514
	 * or {@link handleRequest()}, but in some based we want to set it manually.
515
	 *
516
	 * @param SS_HTTPRequest
517
	 */
518
	public function setRequest($request) {
519
		$this->request = $request;
520
	}
521
}
522