/
Controller.php
587 lines (494 loc) · 19.1 KB
/
Controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
<?php
/**
* @package sapphire
* @subpackage control
*/
/**
* Base controller class.
* Controllers are the cornerstone of all site functionality in Sapphire. The {@link Director}
* selects a controller to pass control to, and then calls {@link run()}. This method will execute
* the appropriate action - either by calling the action method, or displaying the action's template.
*
* See {@link getTemplate()} for information on how the template is chosen.
* @package sapphire
* @subpackage control
*/
class Controller extends ViewableData {
/**
* Define a list of actions that are allowed to be called on this controller.
* The variable should be an array of action names. This sample shows the different values that it can contain:
*
* <code>
* array(
* 'someaction', // someaction can be accessed by anyone, any time
* 'otheraction' => true, // So can otheraction
* 'restrictedaction' => 'ADMIN', // restrictedaction can only be people with ADMIN privilege
* 'complexaction' '->canComplexAction' // complexaction can only be accessed if $this->canComplexAction() returns true
* );
* </code>
*/
static $allowed_actions = null;
protected $urlParams;
protected $requestParams;
protected $action;
/**
* The {@link Session} object for this controller
*/
protected $session;
/**
* Stack of current controllers.
* Controller::$controller_stack[0] is the current controller.
*/
protected static $controller_stack = array();
protected $basicAuthEnabled = true;
/**
* The HTTPResponse object that the controller returns
*/
protected $response;
function setURLParams($urlParams) {
$this->urlParams = $urlParams;
}
/**
* @return array The parameters extracted from the URL by the {@link Director}.
*/
function getURLParams() {
return $this->urlParams;
}
/**
* Returns the HTTPResponse object that this controller is building up.
* Can be used to set the status code and headers
*/
function getResponse() {
return $this->response;
}
protected $baseInitCalled = false;
/**
* Executes this controller, and return an {@link HTTPResponse} object with the result.
*
* This method first does a few set-up activities:
* - Push this controller ont to the controller stack - see {@link Controller::curr()} for information about this.
* - Call {@link init()}
*
* Then it looks for the action method. The action is taken from $this->urlParams['Action'] - for this reason, it's important
* to have $Action included in your Director rule
*
* If $requestParams['executeForm'] is set, then the Controller assumes that we're processing a form. This is usually
* set by adding ?executeForm=XXX to the form's action URL. Form processing differs in the following ways:
* - The action name will be the name of the button clicked. If no button-click can be detected, the first button in the
* list will be assumed.
* - If the given action method doesn't exist on the controller, Controller will look for that method on the Form object.
* this lets developers package both a form and its action handlers in a single subclass of Form.
*
* NOTE: You should rarely need to overload run() - this kind of change is only really appropriate for things like nested
* controllers - {@link ModelAsController} and {@link RootURLController} are two examples here. If you want to make more
* orthodox functionality, it's better to overload {@link init()} or {@link index()}.
*
* Execute the appropriate action handler. If none is given, use defaultAction to display
* a template. The default action will be appropriate in most cases where displaying data
* is the core goal; the Viewer can call methods on the controller to get the data it needs.
*
* @param array $requestParams GET and POST variables.
* @return HTTPResponse The response that this controller produces, including HTTP headers such as redirection info
*/
function run($requestParams) {
if(isset($_GET['debug_profile'])) Profiler::mark("Controller", "run");
$this->pushCurrent();
$this->response = new HTTPResponse();
$this->requestParams = $requestParams;
$this->action = isset($this->urlParams['Action']) ? strtolower(str_replace("-","_",$this->urlParams['Action'])) : "";
if(!$this->action) $this->action = 'index';
// Check security on the controller
if(!$this->checkAccessAction($this->action)) {
user_error("Disallowed action: '$this->action' on controller '$this->class'", E_USER_ERROR);
}
// Init
$this->baseInitCalled = false;
$this->init();
if(!$this->baseInitCalled) user_error("init() method on class '$this->class' doesn't call Controller::init(). Make sure that you have parent::init() included.", E_USER_WARNING);
// If we had a redirection or something, halt processing.
if($this->response->isFinished()) {
$this->popCurrent();
return $this->response;
}
// Look at the action variables for forms
$funcName = null;
foreach($this->requestParams as $paramName => $paramVal) {
if(substr($paramName,0,7) == 'action_') {
// Cleanup action_, _x and _y from image fields
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
break;
}
}
// Form handler
if(isset($this->requestParams['executeForm']) && is_string($this->requestParams['executeForm'])) {
if(isset($funcName)) {
Form::set_current_action($funcName);
}
// Get the appropraite ocntroller: sometimes we want to get a form from another controller
if(isset($this->requestParams['formController'])) {
$formController = Director::getControllerForURL($this->requestParams['formController']);
while(is_a($formController, 'NestedController')) {
$formController = $formController->getNestedController();
}
} else {
$formController = $this;
}
// Create the form object
$form = $formController;
$formObjParts = explode('.', $this->requestParams['executeForm']);
foreach($formObjParts as $formMethod){
if(isset($_GET['debug_profile'])) Profiler::mark("Calling $formMethod", "on $form->class");
$form = $form->$formMethod();
if(isset($_GET['debug_profile'])) Profiler::unmark("Calling $formMethod", "on $form->class");
if(!$form) break; //user_error("Form method '" . $this->requestParams['executeForm'] . "' returns null in controller class '$this->class' ($_SERVER[REQUEST_URI])", E_USER_ERROR);
}
// Populate the form
if(isset($_GET['debug_profile'])) Profiler::mark("Controller", "populate form");
if($form){
$form->loadDataFrom($this->requestParams, true);
// disregard validation if a single field is called
if(!isset($_REQUEST['action_callfieldmethod'])) {
$valid = $form->beforeProcessing();
if(!$valid) {
$this->popCurrent();
return $this->response;
}
}else{
$fieldcaller = $form->dataFieldByName($requestParams['fieldName']);
if(is_a($fieldcaller, "TableListField")){
if($fieldcaller->hasMethod('php')){
$valid = $fieldcaller->php($requestParams);
if(!$valid) exit();
}
}
}
// If the action wasnt' set, choose the default on the form.
if(!isset($funcName) && $defaultAction = $form->defaultAction()){
$funcName = $defaultAction->actionName();
}
if(isset($funcName)) {
$form->setButtonClicked($funcName);
}
}else{
user_error("No form (" . Session::get('CMSMain.currentPage') . ") returned by $formController->class->$_REQUEST[executeForm]", E_USER_WARNING);
}
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller", "populate form");
if(!isset($funcName)) {
user_error("No action button has been clicked in this form executon, and no default has been allowed", E_USER_ERROR);
}
// Protection against CSRF attacks
if($form->securityTokenEnabled()) {
$securityID = Session::get('SecurityID');
if(!$securityID || !isset($this->requestParams['SecurityID']) || $securityID != $this->requestParams['SecurityID']) {
// Don't show error on live sites, as spammers create a million of these
if(Director::isDev()) {
trigger_error("Security ID doesn't match, possible CRSF attack.", E_USER_ERROR);
} else {
die();
}
}
}
// First, try a handler method on the controller
if($this->hasMethod($funcName) || !$form) {
if(isset($_GET['debug_controller'])){
Debug::show("Found function $funcName on the controller");
}
if(isset($_GET['debug_profile'])) Profiler::mark("$this->class::$funcName (controller action)");
$result = $this->$funcName($this->requestParams, $form);
if(isset($_GET['debug_profile'])) Profiler::unmark("$this->class::$funcName (controller action)");
// Otherwise, try a handler method on the form object
} else {
if(isset($_GET['debug_controller'])) {
Debug::show("Found function $funcName on the form object");
}
if(isset($_GET['debug_profile'])) Profiler::mark("$form->class::$funcName (form action)");
$result = $form->$funcName($this->requestParams, $form);
if(isset($_GET['debug_profile'])) Profiler::unmark("$form->class::$funcName (form action)");
}
// Normal action
} else {
if(!isset($funcName)) $funcName = $this->action;
if($this->hasMethod($funcName)) {
if(isset($_GET['debug_controller'])) Debug::show("Found function $funcName on the $this->class controller");
if(isset($_GET['debug_profile'])) Profiler::mark("$this->class::$funcName (controller action)");
$result = $this->$funcName($this->urlParams);
if(isset($_GET['debug_profile'])) Profiler::unmark("$this->class::$funcName (controller action)");
} else {
if(isset($_GET['debug_controller'])) Debug::show("Running default action for $funcName on the $this->class controller" );
if(isset($_GET['debug_profile'])) Profiler::mark("Controller::defaultAction($funcName)");
$result = $this->defaultAction($funcName, $this->urlParams);
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller::defaultAction($funcName)");
}
}
// If your controller function returns an array, then add that data to the
// default template
if(is_array($result)) {
$extended = $this->customise($result);
$viewer = $this->getViewer($funcName);
$result = $viewer->process($extended);
}
$this->response->setBody($result);
if($result) ContentNegotiator::process($this->response);
// Set up HTTP cache headers
HTTP::add_cache_headers($this->response);
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller", "run");
$this->popCurrent();
return $this->response;
}
/**
* This is the default action handler used if a method doesn't exist.
* It will process the controller object with the template returned by {@link getViewer()}
*/
function defaultAction($action) {
return $this->getViewer($action)->process($this);
}
/**
* Returns the action that is being executed on this controller.
*/
function getAction() {
return $this->action;
}
/**
* Return an SSViewer object to process the data
* @return SSViewer The viewer identified being the default handler for this Controller/Action combination
*/
function getViewer($action) {
// Hard-coded templates
if($this->templates[$action]) {
$templates = $this->templates[$action];
} else if($this->templates['index']) {
$templates = $this->templates['index'];
} else if($this->template) {
$templates = $this->template;
} else {
$parentClass = $this->class;
while($parentClass != "Controller") {
$templateName = $parentClass;
if(($pos = strpos($templateName,'_')) !== false) $templateName = substr($templateName, 0, $pos);
if($action && $action != "index") $templates[] = $templateName . '_' . $action;
$templates[] = $templateName;
$parentClass = get_parent_class($parentClass);
}
$templates = array_unique($templates);
}
if(isset($_GET['showtemplate'])) Debug::show($templates);
return new SSViewer($templates);
}
/**
* Call this to disable basic authentication on test sites.
* must be called in the init() method
* @deprecated Use BasicAuth::disable() instead? This is used in CliController - it should be updated.
*/
function disableBasicAuth() {
$this->basicAuthEnabled = false;
}
/**
* Initialisation function that is run before any action on the controller is called.
*/
function init() {
// Test and development sites should be secured, via basic-auth
if(ClassInfo::hasTable("Group") && ClassInfo::hasTable("Member") && Director::isTest() && $this->basicAuthEnabled) {
BasicAuth::requireLogin("SilverStripe test website. Use your CMS login", "ADMIN");
}
//
Cookie::set("PastVisitor", true);
// ClassInfo::hasTable() called to ensure that we're not in a very-first-setup stage
if(ClassInfo::hasTable("Group") && ClassInfo::hasTable("Member") && ($member = Member::currentUser())) {
Cookie::set("PastMember", true);
DB::query("UPDATE Member SET LastVisited = NOW() WHERE ID = $member->ID", null);
}
// This is used to test that subordinate controllers are actually calling parent::init() - a common bug
$this->baseInitCalled = true;
}
/**
* @deprecated use Controller::curr() instead
* @returns Controller
*/
public static function currentController() {
user_error('Controller::currentController() is deprecated. Use Controller::curr() instead.', E_USER_NOTICE);
return self::curr();
}
/**
* Returns the current controller
* @returns Controller
*/
public static function curr() {
if(Controller::$controller_stack) {
return Controller::$controller_stack[0];
} else {
user_error("No current controller available", E_USER_WARNING);
}
}
/**
* Tests whether we have a currently active controller or not
* @return boolean True if there is at least 1 controller in the stack.
*/
public static function has_curr() {
return Controller::$controller_stack ? true : false;
}
/**
* Returns true if the member is allowed to do the given action.
* @param perm The permission to be checked, such as 'View'.
* @param member The member whose permissions need checking. Defaults to the currently logged
* in user.
* @return boolean
* @deprecated I don't believe that the system has widespread use/support of this.
*/
function can($perm, $member = null) {
if(!$member) $member = Member::currentUser();
if($this->hasMethod($methodName = 'can' . $perm)) {
return $this->$methodName($member);
} else {
return true;
}
}
//-----------------------------------------------------------------------------------
/**
* returns a date object for use within a template
* Usage: $Now.Year - Returns 2006
* @return Date The current date
*/
function Now() {
$d = new Date(null);
$d->setVal(date("Y-m-d h:i:s"));
return $d;
}
/**
* Returns a link to any other page
* @deprecated It's unclear what value this has; construct a link manually or use your own custom link-gen functions.
*/
function LinkTo($a, $b) {
return Director::baseURL() . $a . '/' . $b;
}
/**
* Returns an absolute link to this controller
*/
function AbsoluteLink() {
return Director::absoluteURL($this->Link());
}
/**
* Returns the currently logged in user
*/
function CurrentMember() {
return Member::currentUser();
}
/**
* Returns true if the visitor has been here before
* @return boolean
*/
function PastVisitor() {
return Cookie::get("PastVisitor") ? true : false;
}
/**
* Return true if the visitor has signed up for a login account before
* @return boolean
*/
function PastMember() {
return Cookie::get("PastMember") ? true : false;
}
/**
* Pushes this controller onto the stack of current controllers.
* This means that any redirection, session setting, or other things that rely on Controller::curr() will now write to this
* controller object.
*/
function pushCurrent() {
array_unshift(self::$controller_stack, $this);
// Create a new session object
if(!$this->session) {
if(isset(self::$controller_stack[1])) {
$this->session = self::$controller_stack[1]->getSession();
} else {
$this->session = new Session(null);
}
}
}
/**
* Pop this controller off the top of the stack.
*/
function popCurrent() {
if($this === self::$controller_stack[0]) {
array_shift(self::$controller_stack);
} else {
user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack", E_USER_WARNING);
}
}
/**
* Redirct to the given URL.
* It is generally recommended to call Director::redirect() rather than calling this function directly.
*/
function redirect($url) {
if($this->response->getHeader('Location')) {
user_error("Already directed to " . $this->response->getHeader('Location') . "; now trying to direct to $url", E_USER_ERROR);
}
// Attach site-root to relative links, if they have a slash in them
if($url == "" || $url[0] == '?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)){
$url = Director::baseURL() . $url;
}
$this->response->redirect($url);
}
/**
* Tests whether a redirection has been requested.
* @return string If redirect() has been called, it will return the URL redirected to. Otherwise, it will return null;
*/
function redirectedTo() {
return $this->response->getHeader('Location');
}
/**
* Get the Session object representing this Controller's session
* @return Session
*/
function getSession() {
return $this->session;
}
/**
* Set the Session object.
*/
function setSession(Session $session) {
$this->session = $session;
}
/**
* Returns true if this controller is processing an ajax request
* @return boolean True if this controller is processing an ajax request
*/
function isAjax() {
return (
isset($this->requestParams['ajax']) ||
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
);
}
/**
* Check that the given action is allowed to be called on this controller.
* This method is called by run() and makes use of {@link self::$allowed_actions}.
*/
function checkAccessAction($action) {
// Collate self::$allowed_actions from this class and all parent classes
$access = null;
$className = $this->class;
while($className != 'Controller') {
// Merge any non-null parts onto $access.
$accessPart = eval("return $className::\$allowed_actions;");
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
// Build an array of parts for checking if part[0] == part[1], which means that this class doesn't directly define it.
$accessParts[] = $accessPart;
$className = get_parent_class($className);
}
if($access === null || (sizeof($accessParts) > 1 && $accessParts[0] === $accessParts[1])) {
// user_error("Deprecated: please define static \$allowed_actions on your Controllers for security purposes", E_USER_NOTICE);
return true;
}
if($action == 'index') return true;
if(isset($access[$action])) {
$test = $access[$action];
if($test === true) return true;
if(substr($test,0,2) == '->') {
$funcName = substr($test,2);
return $this->$funcName();
}
if(Permission::check($test)) return true;
} else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
return true;
}
return false;
}
}
?>