Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

RequestHandler   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 464
Duplicated Lines 1.29 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 6
loc 464
rs 1.5789
wmc 83
lcom 1
cbo 15

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
/**
4
 * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
5
 *
6
 * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
7
 * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
8
 * controller is being used.  It will then call {@link handleRequest()} on that Controller, passing it the parameters
9
 * that it parsed from the URL, and the {@link SS_HTTPRequest} that contains the remainder of the URL to be parsed.
10
 *
11
 * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
12
 *
13
 * In SilverStripe, URL parsing is distributed throughout the object graph.  For example, suppose that we have a
14
 * search form that contains a {@link TreeMultiSelectField} named "Groups".  We want to use ajax to load segments of
15
 * this tree as they are needed rather than downloading the tree right at the beginning.  We could use this URL to get
16
 * the tree segment that appears underneath
17
 *
18
 * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
19
 *  - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
20
 *    Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
21
 *  - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
22
 *    pass control to that.
23
 *    Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
24
 *  - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
25
 *    control to that.
26
 *    Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
27
 *  - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method.  This method
28
 *    will return an HTML fragment that is output to the screen.
29
 *    Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
30
 *
31
 * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
32
 *
33
 * @package framework
34
 * @subpackage control
35
 */
36
class RequestHandler extends ViewableData {
37
38
	/**
39
	 * @var SS_HTTPRequest $request The request object that the controller was called with.
40
	 * Set in {@link handleRequest()}. Useful to generate the {}
41
	 */
42
	protected $request = null;
43
44
	/**
45
	 * The DataModel for this request
46
	 */
47
	protected $model = null;
48
49
	/**
50
	 * This variable records whether RequestHandler::__construct()
51
	 * was called or not. Useful for checking if subclasses have
52
	 * called parent::__construct()
53
	 *
54
	 * @var boolean
55
	 */
56
	protected $brokenOnConstruct = true;
57
58
	/**
59
	 * The default URL handling rules.  This specifies that the next component of the URL corresponds to a method to
60
	 * be called on this RequestHandlingData object.
61
	 *
62
	 * The keys of this array are parse rules.  See {@link SS_HTTPRequest::match()} for a description of the rules
63
	 * available.
64
	 *
65
	 * The values of the array are the method to be called if the rule matches.  If this value starts with a '$', then
66
	 * the named parameter of the parsed URL wil be used to determine the method name.
67
	 * @config
68
	 */
69
	private static $url_handlers = array(
70
		'$Action' => '$Action',
71
	);
72
73
74
	/**
75
	 * Define a list of action handling methods that are allowed to be called directly by URLs.
76
	 * The variable should be an array of action names. This sample shows the different values that it can contain:
77
	 *
78
	 * <code>
79
	 * array(
80
	 * 		// someaction can be accessed by anyone, any time
81
	 *		'someaction',
82
	 *		// So can otheraction
83
	 *		'otheraction' => true,
84
	 *		// restrictedaction can only be people with ADMIN privilege
85
	 *		'restrictedaction' => 'ADMIN',
86
	 *		// complexaction can only be accessed if $this->canComplexAction() returns true
87
	 *		'complexaction' '->canComplexAction'
88
	 *	);
89
	 * </code>
90
	 *
91
	 * Form getters count as URL actions as well, and should be included in allowed_actions.
92
	 * Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
93
	 * these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
94
	 * either by conditionally removing {@link FormAction} in the form construction,
95
	 * or by defining $allowed_actions in your {@link Form} class.
96
	 * @config
97
	 */
98
	private static $allowed_actions = null;
99
100
	/**
101
	 * @config
102
	 * @var boolean Enforce presence of $allowed_actions when checking acccess.
103
	 * Defaults to TRUE, meaning all URL actions will be denied.
104
	 * When set to FALSE, the controller will allow *all* public methods to be called.
105
	 * In most cases this isn't desireable, and in fact a security risk,
106
	 * since some helper methods can cause side effects which shouldn't be exposed through URLs.
107
	 */
108
	private static $require_allowed_actions = true;
109
110
	public function __construct() {
111
		$this->brokenOnConstruct = false;
112
113
		// Check necessary to avoid class conflicts before manifest is rebuilt
114
		if(class_exists('NullHTTPRequest')) $this->setRequest(new NullHTTPRequest());
115
116
		// This will prevent bugs if setDataModel() isn't called.
117
		$this->model = DataModel::inst();
118
119
		parent::__construct();
120
	}
121
122
	/**
123
	 * Set the DataModel for this request.
124
	 */
125
	public function setDataModel($model) {
126
		$this->model = $model;
127
	}
128
129
	/**
130
	 * Handles URL requests.
131
	 *
132
	 *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
133
	 *  - If the rule matches, the named method will be called.
134
	 *  - If there is still more URL to be processed, then handleRequest()
135
	 *    is called on the object that that method returns.
136
	 *
137
	 * Once all of the URL has been processed, the final result is returned.
138
	 * However, if the final result is an array, this
139
	 * array is interpreted as being additional template data to customise the
140
	 * 2nd to last result with, rather than an object
141
	 * in its own right.  This is most frequently used when a Controller's
142
	 * action will return an array of data with which to
143
	 * customise the controller.
144
	 *
145
	 * @param $request The {@link SS_HTTPRequest} object that is reponsible for distributing URL parsing
146
	 * @uses SS_HTTPRequest
147
	 * @uses SS_HTTPRequest->match()
148
	 * @return SS_HTTPResponse|RequestHandler|string|array
149
	 */
150
	public function handleRequest(SS_HTTPRequest $request, DataModel $model) {
151
		// $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
152
		$handlerClass = ($this->class) ? $this->class : get_class($this);
153
154
		if($this->brokenOnConstruct) {
155
			user_error("parent::__construct() needs to be called on {$handlerClass}::__construct()", E_USER_WARNING);
156
		}
157
158
		$this->setRequest($request);
159
		$this->setDataModel($model);
160
161
		$match = $this->findAction($request);
162
163
		// If nothing matches, return this object
164
		if (!$match) return $this;
165
166
		// Start to find what action to call. Start by using what findAction returned
167
		$action = $match['action'];
168
169
		// We used to put "handleAction" as the action on controllers, but (a) this could only be called when
170
		// you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
171
		if ($action == 'handleAction') {
172
			// TODO Fix LeftAndMain usage
173
			// Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
174
			$action = '$Action';
175
		}
176
177
		// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
178
		if($action[0] == '$') {
179
			$action = str_replace("-", "_", $request->latestParam(substr($action,1)));
180
		}
181
182
		if(!$action) {
183
			if(isset($_REQUEST['debug_request'])) {
184
				Debug::message("Action not set; using default action method name 'index'");
185
			}
186
			$action = "index";
187 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...
188
			user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
189
		}
190
191
		$classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
192
193
		try {
194
			if(!$this->hasAction($action)) {
195
				return $this->httpError(404, "Action '$action' isn't available $classMessage.");
196
			}
197
			if(!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'init'))) {
198
				return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
199
			}
200
			$result = $this->handleAction($request, $action);
201
		}
202
		catch (SS_HTTPResponse_Exception $e) {
203
			return $e->getResponse();
204
		}
205
		catch(PermissionFailureException $e) {
206
			$result = Security::permissionFailure(null, $e->getMessage());
207
		}
208
209
		if($result instanceof SS_HTTPResponse && $result->isError()) {
210
			if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
211
			return $result;
212
		}
213
214
		// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
215
		// parse. It might have its own handler. However, we only do this if we haven't just parsed an
216
		// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
217
		// actions which return themselves to avoid infinite loops.
218
		$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
219
		$resultIsRequestHandler = is_object($result) && $result instanceof RequestHandler;
220
221
		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...
222
			$returnValue = $result->handleRequest($request, $model);
223
224
			// Array results can be used to handle
225
			if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
226
227
			return $returnValue;
228
229
		// If we return some other data, and all the URL is parsed, then return that
230
		} else if($request->allParsed()) {
231
			return $result;
232
233
		// But if we have more content on the URL and we don't know what to do with it, return an error.
234
		} else {
235
			return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
236
		}
237
238
		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...
239
	}
240
241
	protected function findAction($request) {
242
		$handlerClass = ($this->class) ? $this->class : get_class($this);
243
244
		// We stop after RequestHandler; in other words, at ViewableData
245
		while($handlerClass && $handlerClass != 'ViewableData') {
246
			$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
247
248
			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...
249 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...
250
					Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
251
				}
252
253
				if($request->match($rule, true)) {
254
					if(isset($_REQUEST['debug_request'])) {
255
						Debug::message(
256
							"Rule '$rule' matched to action '$action' on $this->class. ".
257
							"Latest request params: " . var_export($request->latestParams(), true)
258
						);
259
					}
260
261
					return array('rule' => $rule, 'action' => $action);
262
				}
263
			}
264
265
			$handlerClass = get_parent_class($handlerClass);
266
		}
267
	}
268
269
	/**
270
	 * Given a request, and an action name, call that action name on this RequestHandler
271
	 *
272
	 * Must not raise SS_HTTPResponse_Exceptions - instead it should return
273
	 *
274
	 * @param $request
275
	 * @param $action
276
	 * @return SS_HTTPResponse
277
	 */
278
	protected function handleAction($request, $action) {
279
		$classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
280
281
		if(!$this->hasMethod($action)) {
282
			return new SS_HTTPResponse("Action '$action' isn't available $classMessage.", 404);
283
		}
284
285
		$res = $this->extend('beforeCallActionHandler', $request, $action);
286
		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...
287
288
		$actionRes = $this->$action($request);
289
290
		$res = $this->extend('afterCallActionHandler', $request, $action);
291
		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...
292
293
		return $actionRes;
294
	}
295
296
	/**
297
	 * Get a array of allowed actions defined on this controller,
298
	 * any parent classes or extensions.
299
	 *
300
	 * Caution: Since 3.1, allowed_actions definitions only apply
301
	 * to methods on the controller they're defined on,
302
	 * so it is recommended to use the $class argument
303
	 * when invoking this method.
304
	 *
305
	 * @param String $limitToClass
306
	 * @return array|null
307
	 */
308
	public function allowedActions($limitToClass = null) {
309
		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...
310
			$actions = Config::inst()->get(
311
				$limitToClass,
312
				'allowed_actions',
313
				Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
314
			);
315
		} else {
316
			$actions = Config::inst()->get(get_class($this), 'allowed_actions');
317
		}
318
319
		if(is_array($actions)) {
320
			if(array_key_exists('*', $actions)) {
321
				Deprecation::notice(
322
					'3.0',
323
					'Wildcards (*) are no longer valid in $allowed_actions due their ambiguous '
324
					. ' and potentially insecure behaviour. Please define all methods explicitly instead.'
325
				);
326
			}
327
328
			// convert all keys and values to lowercase to
329
			// allow for easier comparison, unless it is a permission code
330
			$actions = array_change_key_case($actions, CASE_LOWER);
331
332
			foreach($actions as $key => $value) {
333
				if(is_numeric($key)) $actions[$key] = strtolower($value);
334
			}
335
336
			return $actions;
337
		} else {
338
			return null;
339
		}
340
	}
341
342
	/**
343
	 * Checks if this request handler has a specific action,
344
	 * even if the current user cannot access it.
345
	 * Includes class ancestry and extensions in the checks.
346
	 *
347
	 * @param string $action
348
	 * @return bool
349
	 */
350
	public function hasAction($action) {
351
		if($action == 'index') return true;
352
353
		// Don't allow access to any non-public methods (inspect instance plus all extensions)
354
		$insts = array_merge(array($this), (array) $this->getExtensionInstances());
355
		foreach($insts as $inst) {
356
			if(!method_exists($inst, $action)) continue;
357
			$r = new ReflectionClass(get_class($inst));
358
			$m = $r->getMethod($action);
359
			if(!$m || !$m->isPublic()) return false;
360
		}
361
362
		$action  = strtolower($action);
363
		$actions = $this->allowedActions();
364
365
		// Check if the action is defined in the allowed actions of any ancestry class
366
		// as either a key or value. Note that if the action is numeric, then keys are not
367
		// searched for actions to prevent actual array keys being recognised as actions.
368
		if(is_array($actions)) {
369
			$isKey   = !is_numeric($action) && array_key_exists($action, $actions);
370
			$isValue = in_array($action, $actions, true);
371
			if($isKey || $isValue) return true;
372
		}
373
374
		$actionsWithoutExtra = $this->config()->get(
375
			'allowed_actions',
376
			Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
377
		);
378
		if(!is_array($actions) || !$actionsWithoutExtra) {
379
			if($action != 'init' && $action != 'run' && method_exists($this, $action)) return true;
380
		}
381
382
		return false;
383
	}
384
385
	/**
386
	 * Return the class that defines the given action, so that we know where to check allowed_actions.
387
	 */
388
	protected function definingClassForAction($actionOrigCasing) {
389
		$action = strtolower($actionOrigCasing);
390
391
		$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...
392
		$insts = array_merge(array($this), (array) $this->getExtensionInstances());
393
		foreach($insts as $inst) {
394
			if(!method_exists($inst, $action)) continue;
395
			$r = new ReflectionClass(get_class($inst));
396
			$m = $r->getMethod($actionOrigCasing);
397
			return $m->getDeclaringClass()->getName();
398
		}
399
	}
400
401
	/**
402
	 * Check that the given action is allowed to be called from a URL.
403
	 * It will interrogate {@link self::$allowed_actions} to determine this.
404
	 */
405
	public function checkAccessAction($action) {
406
		$actionOrigCasing = $action;
407
		$action = strtolower($action);
408
409
		$isAllowed = false;
410
		$isDefined = false;
411
412
		// Get actions for this specific class (without inheritance)
413
		$definingClass = $this->definingClassForAction($actionOrigCasing);
414
		$allowedActions = $this->allowedActions($definingClass);
415
416
		// check if specific action is set
417
		if(isset($allowedActions[$action])) {
418
			$isDefined = true;
419
			$test = $allowedActions[$action];
420
			if($test === true || $test === 1 || $test === '1') {
421
				// TRUE should always allow access
422
				$isAllowed = true;
423
			} elseif(substr($test, 0, 2) == '->') {
424
				// Determined by custom method with "->" prefix
425
				list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
426
				$isAllowed = call_user_func_array(array($this, $method), $arguments);
427
			} else {
428
				// Value is a permission code to check the current member against
429
				$isAllowed = Permission::check($test);
430
			}
431
		} elseif(
432
			is_array($allowedActions)
433
			&& (($key = array_search($action, $allowedActions, true)) !== false)
434
			&& is_numeric($key)
435
		) {
436
			// Allow numeric array notation (search for array value as action instead of key)
437
			$isDefined = true;
438
			$isAllowed = true;
439
		} elseif(is_array($allowedActions) && !count($allowedActions)) {
440
			// If defined as empty array, deny action
441
			$isAllowed = false;
442
		} elseif($allowedActions === null) {
443
			// If undefined, allow action based on configuration
444
			$isAllowed = !Config::inst()->get('RequestHandler', 'require_allowed_actions');
445
		}
446
447
		// If we don't have a match in allowed_actions,
448
		// whitelist the 'index' action as well as undefined actions based on configuration.
449
		if(!$isDefined && ($action == 'index' || empty($action))) {
450
			$isAllowed = true;
451
		}
452
453
		return $isAllowed;
454
	}
455
456
	/**
457
	 * Throws a HTTP error response encased in a {@link SS_HTTPResponse_Exception}, which is later caught in
458
	 * {@link RequestHandler::handleAction()} and returned to the user.
459
	 *
460
	 * @param int $errorCode
461
	 * @param string $errorMessage Plaintext error message
462
	 * @uses SS_HTTPResponse_Exception
463
	 */
464
	public function httpError($errorCode, $errorMessage = null) {
465
466
		$request = $this->getRequest();
467
468
		// Call a handler method such as onBeforeHTTPError404
469
		$this->extend('onBeforeHTTPError' . $errorCode, $request);
470
471
		// Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
472
		$this->extend('onBeforeHTTPError', $errorCode, $request);
473
474
		// Throw a new exception
475
		throw new SS_HTTPResponse_Exception($errorMessage, $errorCode);
476
	}
477
478
	/**
479
	 * Returns the SS_HTTPRequest object that this controller is using.
480
	 * Returns a placeholder {@link NullHTTPRequest} object unless
481
	 * {@link handleAction()} or {@link handleRequest()} have been called,
482
	 * which adds a reference to an actual {@link SS_HTTPRequest} object.
483
	 *
484
	 * @return SS_HTTPRequest|NullHTTPRequest
485
	 */
486
	public function getRequest() {
487
		return $this->request;
488
	}
489
490
	/**
491
	 * Typically the request is set through {@link handleAction()}
492
	 * or {@link handleRequest()}, but in some based we want to set it manually.
493
	 *
494
	 * @param SS_HTTPRequest
495
	 */
496
	public function setRequest($request) {
497
		$this->request = $request;
498
	}
499
}
500