1 <?php
2 /**
3 * Base controller class.
4 * Controllers are the cornerstone of all site functionality in Sapphire. The {@link Director}
5 * selects a controller to pass control to, and then calls {@link run()}. This method will execute
6 * the appropriate action - either by calling the action method, or displaying the action's template.
7 *
8 * See {@link getTemplate()} for information on how the template is chosen.
9 * @package sapphire
10 * @subpackage control
11 */
12 class Controller extends RequestHandler {
13
14 /**
15 * @var array $urlParams An array of arguments extracted from the URL
16 */
17 protected $urlParams;
18
19 /**
20 * @var array $requestParams Contains all GET and POST parameters
21 * passed to the current {@link SS_HTTPRequest}.
22 * @uses SS_HTTPRequest->requestVars()
23 */
24 protected $requestParams;
25
26 /**
27 * @var string $action The URL part matched on the current controller as
28 * determined by the "$Action" part of the {@link $url_handlers} definition.
29 * Should correlate to a public method on this controller.
30 * Used in {@link render()} and {@link getViewer()} to determine
31 * action-specific templates.
32 */
33 protected $action;
34
35 /**
36 * The {@link Session} object for this controller
37 */
38 protected $session;
39
40 /**
41 * Stack of current controllers.
42 * Controller::$controller_stack[0] is the current controller.
43 */
44 protected static $controller_stack = array();
45
46 protected $basicAuthEnabled = true;
47
48 /**
49 * @var SS_HTTPResponse $response The response object that the controller returns.
50 * Set in {@link handleRequest()}.
51 */
52 protected $response;
53
54 /**
55 * @var SS_HTTPRequest $request The request object that the controller was called with.
56 * Set in {@link handleRequest()}. Useful to generate the {}
57 */
58 protected $request;
59
60 /**
61 * Default URL handlers - (Action)/(ID)/(OtherID)
62 */
63 static $url_handlers = array(
64 '$Action//$ID/$OtherID' => 'handleAction',
65 );
66
67 static $allowed_actions = array(
68 'handleAction',
69 'handleIndex',
70 );
71
72 /**
73 * Initialisation function that is run before any action on the controller is called.
74 *
75 * @uses BasicAuth::requireLogin()
76 */
77 function init() {
78 if($this->basicAuthEnabled) BasicAuth::protect_site_if_necessary();
79
80 // Directly access the session variable just in case the Group or Member tables don't yet exist
81 if(Session::get('loggedInAs') && Security::database_is_ready()) {
82 $member = Member::currentUser();
83 if($member) {
84 Cookie::set("PastMember", true, 90, null, null, false, true);
85 DB::query("UPDATE \"Member\" SET \"LastVisited\" = " . DB::getConn()->now() . " WHERE \"ID\" = $member->ID", null);
86 }
87 }
88
89 // This is used to test that subordinate controllers are actually calling parent::init() - a common bug
90 $this->baseInitCalled = true;
91 }
92
93 /**
94 * Executes this controller, and return an {@link SS_HTTPResponse} object with the result.
95 *
96 * This method first does a few set-up activities:
97 * - Push this controller ont to the controller stack -
98 * see {@link Controller::curr()} for information about this.
99 * - Call {@link init()}
100 * - Defer to {@link RequestHandler->handleRequest()} to determine which action
101 * should be executed
102 *
103 * Note: $requestParams['executeForm'] support was removed,
104 * make the following change in your URLs:
105 * "/?executeForm=FooBar" -> "/FooBar"
106 * Also make sure "FooBar" is in the $allowed_actions of your controller class.
107 *
108 * Note: You should rarely need to overload run() -
109 * this kind of change is only really appropriate for things like nested
110 * controllers - {@link ModelAsController} and {@link RootURLController}
111 * are two examples here. If you want to make more
112 * orthodox functionality, it's better to overload {@link init()} or {@link index()}.
113 *
114 * Important: If you are going to overload handleRequest,
115 * make sure that you start the method with $this->pushCurrent()
116 * and end the method with $this->popCurrent().
117 * Failure to do this will create weird session errors.
118 *
119 * @param $request The {@link SS_HTTPRequest} object that is responsible
120 * for distributing request parsing.
121 * @return SS_HTTPResponse The response that this controller produces,
122 * including HTTP headers such as redirection info
123 */
124 function handleRequest(SS_HTTPRequest $request) {
125 if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
126
127 $this->pushCurrent();
128 $this->urlParams = $request->allParams();
129 $this->request = $request;
130 $this->response = new SS_HTTPResponse();
131
132 $this->extend('onBeforeInit');
133
134 // Init
135 $this->baseInitCalled = false;
136 $this->init();
137 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);
138
139 $this->extend('onAfterInit');
140
141 // If we had a redirection or something, halt processing.
142 if($this->response->isFinished()) {
143 $this->popCurrent();
144 return $this->response;
145 }
146
147 $body = parent::handleRequest($request);
148 if($body instanceof SS_HTTPResponse) {
149 if(isset($_REQUEST['debug_request'])) Debug::message("Request handler returned SS_HTTPResponse object to $this->class controller; returning it without modification.");
150 $this->response = $body;
151
152 } else {
153 if(is_object($body)) {
154 if(isset($_REQUEST['debug_request'])) Debug::message("Request handler $body->class object to $this->class controller;, rendering with template returned by $body->class::getViewer()");
155 $body = $body->getViewer($request->latestParam('Action'))->process($body);
156 }
157
158 $this->response->setBody($body);
159 }
160
161
162 ContentNegotiator::process($this->response);
163 HTTP::add_cache_headers($this->response);
164
165 $this->popCurrent();
166 return $this->response;
167 }
168
169 /**
170 * Controller's default action handler. It will call the method named in $Action, if that method exists.
171 * If $Action isn't given, it will use "index" as a default.
172 */
173 public function handleAction($request) {
174 // urlParams, requestParams, and action are set for backward compatability
175 foreach($request->latestParams() as $k => $v) {
176 if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
177 }
178
179 $this->action = str_replace("-","_",$request->param('Action'));
180 $this->requestParams = $request->requestVars();
181 if(!$this->action) $this->action = 'index';
182
183 if(!$this->hasAction($this->action)) {
184 $hasAlternateURL = false;
185 $this->extend('updateAlternateURL', $hasAlternateURL, $request);
186 if ($hasAlternateURL) {
187 return $this->redirect($hasAlternateURL);
188 }
189 $this->httpError(404, "The action '$this->action' does not exist in class $this->class");
190 }
191
192 // run & init are manually disabled, because they create infinite loops and other dodgy situations
193 if(!$this->checkAccessAction($this->action) || in_array(strtolower($this->action), array('run', 'init'))) {
194 return $this->httpError(403, "Action '$this->action' isn't allowed on class $this->class");
195 }
196
197 if($this->hasMethod($this->action)) {
198 $result = $this->{$this->action}($request);
199
200 // If the action returns an array, customise with it before rendering the template.
201 if(is_array($result)) {
202 return $this->getViewer($this->action)->process($this->customise($result));
203 } else {
204 return $result;
205 }
206 } else {
207 return $this->getViewer($this->action)->process($this);
208 }
209 }
210
211 function setURLParams($urlParams) {
212 $this->urlParams = $urlParams;
213 }
214
215 /**
216 * @return array The parameters extracted from the URL by the {@link Director}.
217 */
218 function getURLParams() {
219 return $this->urlParams;
220 }
221
222 /**
223 * Returns the SS_HTTPResponse object that this controller is building up.
224 * Can be used to set the status code and headers
225 */
226 function getResponse() {
227 return $this->response;
228 }
229
230 /**
231 * Get the request with which this controller was called (if any).
232 * Usually set in {@link handleRequest()}.
233 *
234 * @return SS_HTTPRequest
235 */
236 function getRequest() {
237 return $this->request;
238 }
239
240 protected $baseInitCalled = false;
241
242 /**
243 * Return the object that is going to own a form that's being processed, and handle its execution.
244 * Note that the result needn't be an actual controller object.
245 */
246 function getFormOwner() {
247 // Get the appropraite ocntroller: sometimes we want to get a form from another controller
248 if(isset($this->requestParams['formController'])) {
249 $formController = Director::getControllerForURL($this->requestParams['formController']);
250
251 while(is_a($formController, 'NestedController')) {
252 $formController = $formController->getNestedController();
253 }
254 return $formController;
255
256 } else {
257 return $this;
258 }
259 }
260
261 /**
262 * This is the default action handler used if a method doesn't exist.
263 * It will process the controller object with the template returned by {@link getViewer()}
264 */
265 function defaultAction($action) {
266 return $this->getViewer($action)->process($this);
267 }
268
269 /**
270 * Returns the action that is being executed on this controller.
271 */
272 function getAction() {
273 return $this->action;
274 }
275
276 /**
277 * Return an SSViewer object to process the data
278 * @return SSViewer The viewer identified being the default handler for this Controller/Action combination
279 */
280 function getViewer($action) {
281 // Hard-coded templates
282 if($this->templates[$action]) {
283 $templates = $this->templates[$action];
284 } else if($this->templates['index']) {
285 $templates = $this->templates['index'];
286 } else if($this->template) {
287 $templates = $this->template;
288 } else {
289 // Add action-specific templates for inheritance chain
290 $parentClass = $this->class;
291 if($action && $action != 'index') {
292 $parentClass = $this->class;
293 while($parentClass != "Controller") {
294 $template = strtok($parentClass,'_') . '_' . $action;
295 if ($this->isAjax()) {
296 $templates[] = $template . '_ajax';
297 }
298 $templates[] = $template;
299 $parentClass = get_parent_class($parentClass);
300 }
301 }
302 // Add controller templates for inheritance chain
303 $parentClass = $this->class;
304 while($parentClass != "Controller") {
305 $template = strtok($parentClass,'_');
306 if ($this->isAjax()) {
307 $templates[] = $template . '_ajax';
308 }
309 $templates[] = $template;
310 $parentClass = get_parent_class($parentClass);
311 }
312
313 // remove duplicates
314 $templates = array_unique($templates);
315 }
316 return new SSViewer($templates);
317 }
318
319 public function hasAction($action) {
320 return parent::hasAction($action) || $this->hasActionTemplate($action);
321 }
322
323 /**
324 * Returns TRUE if this controller has a template that is specifically designed to handle a specific action.
325 *
326 * @param string $action
327 * @return bool
328 */
329 public function hasActionTemplate($action) {
330 if(isset($this->templates[$action])) return true;
331
332 $parentClass = $this->class;
333 $templates = array();
334
335 while($parentClass != 'Controller') {
336 $templates[] = strtok($parentClass, '_') . '_' . $action;
337 $parentClass = get_parent_class($parentClass);
338 }
339
340 return SSViewer::hasTemplate($templates);
341 }
342
343 /**
344 * Render the current controller with the templates determined
345 * by {@link getViewer()}.
346 *
347 * @param array $params Key-value array for custom template variables (Optional)
348 * @return string Parsed template content
349 */
350 function render($params = null) {
351 $template = $this->getViewer($this->getAction());
352
353 // if the object is already customised (e.g. through Controller->run()), use it
354 $obj = ($this->customisedObj) ? $this->customisedObj : $this;
355
356 if($params) $obj = $this->customise($params);
357
358 return $template->process($obj);
359 }
360
361 /**
362 * Call this to disable site-wide basic authentication for a specific contoller.
363 * This must be called before Controller::init(). That is, you must call it in your controller's
364 * init method before it calls parent::init().
365 */
366 function disableBasicAuth() {
367 $this->basicAuthEnabled = false;
368 }
369
370 /**
371 * Returns the current controller
372 * @returns Controller
373 */
374 public static function curr() {
375 if(Controller::$controller_stack) {
376 return Controller::$controller_stack[0];
377 } else {
378 user_error("No current controller available", E_USER_WARNING);
379 }
380 }
381
382 /**
383 * Tests whether we have a currently active controller or not
384 * @return boolean True if there is at least 1 controller in the stack.
385 */
386 public static function has_curr() {
387 return Controller::$controller_stack ? true : false;
388 }
389
390 /**
391 * Returns true if the member is allowed to do the given action.
392 * @param perm The permission to be checked, such as 'View'.
393 * @param member The member whose permissions need checking. Defaults to the currently logged
394 * in user.
395 * @return boolean
396 */
397 function can($perm, $member = null) {
398 if(!$member) $member = Member::currentUser();
399 if($this->hasMethod($methodName = 'can' . $perm)) {
400 return $this->$methodName($member);
401 } else {
402 return true;
403 }
404 }
405
406 //-----------------------------------------------------------------------------------
407
408 /**
409 * returns a date object for use within a template
410 * Usage: $Now.Year - Returns 2006
411 * @return Date The current date
412 */
413 function Now() {
414 $d = new Date(null);
415 $d->setValue(date("Y-m-d h:i:s"));
416 return $d;
417 }
418
419 /**
420 * Returns the currently logged in user
421 */
422 function CurrentMember() {
423 return Member::currentUser();
424 }
425
426 /**
427 * Returns true if the visitor has been here before
428 * @return boolean
429 */
430 function PastVisitor() {
431 user_error("Controller::PastVisitor() is deprecated", E_USER_NOTICE);
432 return false;
433 }
434
435 /**
436 * Return true if the visitor has signed up for a login account before
437 * @return boolean
438 */
439 function PastMember() {
440 return Cookie::get("PastMember") ? true : false;
441 }
442
443 /**
444 * Pushes this controller onto the stack of current controllers.
445 * This means that any redirection, session setting, or other things that rely on Controller::curr() will now write to this
446 * controller object.
447 */
448 function pushCurrent() {
449 array_unshift(self::$controller_stack, $this);
450 // Create a new session object
451 if(!$this->session) {
452 if(isset(self::$controller_stack[1])) {
453 $this->session = self::$controller_stack[1]->getSession();
454 } else {
455 $this->session = new Session(Session::get_all());
456 }
457 }
458 }
459
460 /**
461 * Pop this controller off the top of the stack.
462 */
463 function popCurrent() {
464 if($this === self::$controller_stack[0]) {
465 array_shift(self::$controller_stack);
466 } else {
467 user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack", E_USER_WARNING);
468 }
469 }
470
471 /**
472 * Redirct to the given URL.
473 * It is generally recommended to call Director::redirect() rather than calling this function directly.
474 */
475 function redirect($url, $code=302) {
476 if($this->response->getHeader('Location')) {
477 user_error("Already directed to " . $this->response->getHeader('Location') . "; now trying to direct to $url", E_USER_WARNING);
478 return;
479 }
480
481 // Attach site-root to relative links, if they have a slash in them
482 if($url == "" || $url[0] == '?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)){
483 $url = Director::baseURL() . $url;
484 }
485
486 $this->response->redirect($url, $code);
487 }
488
489 /**
490 * Redirect back. Uses either the HTTP_REFERER or a manually set request-variable called
491 * _REDIRECT_BACK_URL.
492 * This variable is needed in scenarios where not HTTP-Referer is sent (
493 * e.g when calling a page by location.href in IE).
494 * If none of the two variables is available, it will redirect to the base
495 * URL (see {@link Director::baseURL()}).
496 * @uses redirect()
497 */
498 function redirectBack() {
499 if($this->request->requestVar('_REDIRECT_BACK_URL')) {
500 $url = $this->request->requestVar('_REDIRECT_BACK_URL');
501 } else if($this->request->getHeader('Referer')) {
502 $url = $this->request->getHeader('Referer');
503 } else {
504 $url = Director::baseURL();
505 }
506
507 // absolute redirection URLs not located on this site may cause phishing
508 if(Director::is_site_url($url)) {
509 return $this->redirect($url);
510 } else {
511 return false;
512 }
513
514 }
515
516 /**
517 * Tests whether a redirection has been requested.
518 * @return string If redirect() has been called, it will return the URL redirected to. Otherwise, it will return null;
519 */
520 function redirectedTo() {
521 return $this->response->getHeader('Location');
522 }
523
524 /**
525 * Get the Session object representing this Controller's session
526 * @return Session
527 */
528 function getSession() {
529 return $this->session;
530 }
531
532 /**
533 * Set the Session object.
534 */
535 function setSession(Session $session) {
536 $this->session = $session;
537 }
538
539 /**
540 * Returns true if this controller is processing an ajax request
541 * @return boolean True if this controller is processing an ajax request
542 */
543 function isAjax() {
544 return (
545 isset($this->requestParams['ajax']) || isset($_REQUEST['ajax']) ||
546 (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
547 );
548 }
549
550 /**
551 * Joins two or more link segments together, putting a slash between them if necessary.
552 * Use this for building the results of {@link Link()} methods.
553 * If either of the links have query strings,
554 * then they will be combined and put at the end of the resulting url.
555 *
556 * Caution: All parameters are expected to be URI-encoded already.
557 *
558 * @param String
559 * @return String
560 */
561 static function join_links() {
562 $args = func_get_args();
563 $result = "";
564 $querystrings = array();
565 foreach($args as $arg) {
566 if(strpos($arg,'?') !== false) {
567 list($arg, $suffix) = explode('?',$arg,2);
568 $querystrings[] = $suffix;
569 }
570 if($arg) {
571 if($result && substr($result,-1) != '/' && $arg[0] != '/') $result .= "/$arg";
572 else $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
573 }
574 }
575
576 if($querystrings) $result .= '?' . implode('&', $querystrings);
577
578 return $result;
579 }
580 }
581
582 ?>
583