Adding extra charsets for ActionMailer unit tests, if you're looking to parse incomin...
[akelos.git] / lib / AkRequest.php
blob783cfce7259086dc930c7e4d8db4b77ef4b14e73
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 // +----------------------------------------------------------------------+
5 // | Akelos Framework - http://www.akelos.org |
6 // +----------------------------------------------------------------------+
7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
8 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
9 // +----------------------------------------------------------------------+
11 /**
12 * @package ActionController
13 * @subpackage Request
14 * @author Bermi Ferrer <bermi a.t akelos c.om>
15 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
16 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
19 if(!defined('AK_DEFAULT_CONTROLLER')){
20 define('AK_DEFAULT_CONTROLLER', 'page');
22 if(!defined('AK_DEFAULT_ACTION')){
23 define('AK_DEFAULT_ACTION', 'index');
26 defined('AK_HIGH_LOAD_MODE') ? null : define('AK_HIGH_LOAD_MODE', false);
27 defined('AK_AUTOMATIC_DB_CONNECTION') ? null : define('AK_AUTOMATIC_DB_CONNECTION', !AK_HIGH_LOAD_MODE);
28 defined('AK_AUTOMATIC_SESSION_START') ? null : define('AK_AUTOMATIC_SESSION_START', !AK_HIGH_LOAD_MODE);
30 // IIS does not provide a valid REQUEST_URI so we need to guess it from the script name + query string
31 $_SERVER['REQUEST_URI'] = (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME'].(( isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '')));
34 /**
35 * Class that handles incoming request.
37 * The Request Object handles user request (CLI, GET, POST, session or
38 * cookie requests), transforms it and sets it up for the
39 * ApplicationController class, who takes control of the data
40 * flow.
42 * @author Bermi Ferrer <bermi@akelos.com>
43 * @copyright Copyright (c) 2002-2005, Akelos Media, S.L. http://www.akelos.org
44 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
46 class AkRequest extends AkObject
49 /**
50 * Array containing the request parameters.
52 * This property stores the parameters parsed from the
53 * parseRequest() method. This array is used by addParams()
54 * method.
56 * @access private
57 * @var array $_request
59 var $_request = array();
61 var $_init_check = false;
62 var $__internationalization_support_enabled = false;
64 var $action = AK_DEFAULT_ACTION;
65 var $controller = AK_DEFAULT_CONTROLLER;
66 var $view;
68 /**
69 * Holds information about current environment. Initially a reference to $_SERVER
71 * @var array
73 var $env = array();
76 /**
77 * String parse method.
79 * This method gets a petition as parameter, using the "Ruby
80 * on Rails" request format (see prettyURL in RoR documentation). The format is:
81 * file.php?ak=/controller/action/id&paramN=valueN
83 * This method requires for a previous execution of the _mergeRequest() method,
84 * in order to merge all the request all i one array.
86 * This method expands dynamically the class Request, adding a public property for
87 * every parameter sent in the request.
90 * @access public
91 * @return array
93 function _parseAkRequestString($ak_request_string, $pattern = '/')
95 $result = array();
96 $ak_request = trim($ak_request_string,$pattern);
97 if(strstr($ak_request,$pattern)){
98 $result = explode($pattern,$ak_request);
100 return $result;
104 function __construct ()
106 $this->init();
112 * Initialization method.
114 * Initialization method. Use this via the class constructor.
116 * @access public
117 * @uses parseRequest
118 * @return void
120 function init()
122 if(!$this->_init_check){
123 $this->env =& $_SERVER;
124 $this->_fixGpcMagic();
125 $this->_urlDecode();
127 $this->_mergeRequest();
129 if(is_array($this->_request)){
130 foreach ($this->_request as $k=>$v){
131 $this->_addParam($k, $v);
135 $this->_init_check = true;
138 if(defined('AK_LOG_EVENTS') && AK_LOG_EVENTS){
139 $this->Logger =& Ak::getLogger();
140 $this->Logger->message($this->Logger->formatText('Request','green').' from '.$this->getRemoteIp(), $this->getParams());
144 function get($var_name)
146 return $this->_request[$var_name];
149 function getParams()
151 return array_merge(array('controller'=>$this->controller,'action'=>$this->action),$this->_request);
154 function getAction()
156 return $this->action;
159 function getController()
161 return $this->controller;
164 function reset()
166 $this->_request = array();
167 $this->_init_check = false;
170 function set($variable, $value)
172 $this->_addParam($variable, $value);
176 function checkForRoutedRequests(&$Router)
178 $ak_request = isset($this->_request['ak']) ? '/'.trim($this->_request['ak'],'/').'/' : '/';
180 if($found = $Router->toParams($ak_request)){
181 if(!isset($found['controller'])){
182 trigger_error(Ak::t('No controller was specified.'), E_USER_WARNING);
184 if(!isset($found['action'])){
185 trigger_error(Ak::t('No action was specified.'), E_USER_WARNING);
188 if(isset($found['controller'])){
189 if($this->_addParam('controller',$found['controller'])){
190 $this->controller = $this->_request['controller'] = $found['controller'];
193 if(isset($found['action'])){
194 if($this->_addParam('action',$found['action'])){
195 $this->action = $this->_request['action'] = $found['action'];
198 if(isset($found['module'])){
199 if($this->_addParam('module',$found['module'])){
200 $this->module = $this->_request['module'] = $found['module'];
204 foreach ($found as $k=>$v){
205 if($this->_addParam($k,$v)){
206 $this->_request[$k] = $v;
213 function isValidControllerName($controller_name)
215 return $this->_validateTechName($controller_name);
218 function isValidActionName($action_name)
220 return $this->_validateTechName($action_name);
223 function isValidModuleName($module_name)
225 return preg_match('/^[A-Za-z]{1,}[A-Za-z0-9_\/]*$/', $module_name);
231 * Returns both GET and POST parameters in a single array.
233 function getParameters()
235 if(empty($this->parameters)){
236 $this->parameters = $this->getParams();
238 return $this->parameters;
241 function setPathParameters($parameters)
243 $this->_path_parameters = $parameters;
246 function getPathParameters()
248 return empty($this->_path_parameters) ? array() : $this->_path_parameters;
251 function getUrlParams()
253 return $_GET;
257 * Must be implemented in the concrete request
259 function getQueryParameters ()
262 function getRequestParameters ()
267 * Returns the path minus the web server relative installation directory. This method returns null unless the web server is apache.
269 function getRelativeUrlRoot()
271 return str_replace('/index.php','', @$this->env['PHP_SELF']);
275 * Returns the locale identifier of current URL
277 function getLocaleFromUrl()
279 $locale = Ak::get_url_locale();
280 if(strstr(AK_CURRENT_URL,AK_SITE_URL.'/'.$locale)){
281 return $locale;
283 return '';
287 * Returns the HTTP request method as a lowercase symbol ('get, for example)
289 function getMethod()
291 return strtolower($this->env['REQUEST_METHOD']);
295 * Is this a GET request? Equivalent to $Request->getMethod() == 'get'
297 function isGet()
299 return $this->getMethod() == 'get';
303 * Is this a POST request? Equivalent to $Request->getMethod() == 'post'
305 function isPost()
307 return $this->getMethod() == 'post';
311 * Is this a PUT request? Equivalent to $Request->getMethod() == 'put'
313 function isPut()
315 return isset($this->env['REQUEST_METHOD']) ? $this->getMethod() == 'put' : false;
319 * Is this a DELETE request? Equivalent to $Request->getMethod() == 'delete'
321 function isDelete()
323 return $this->getMethod() == 'delete';
327 * Is this a HEAD request? Equivalent to $Request->getMethod() == 'head'
329 function isHead()
331 return $this->getMethod() == 'head';
337 * Determine originating IP address. REMOTE_ADDR is the standard
338 * but will fail if( the user is behind a proxy. HTTP_CLIENT_IP and/or
339 * HTTP_X_FORWARDED_FOR are set by proxies so check for these before
340 * falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
341 * delimited list in the case of multiple chained proxies; the first is
342 * the originating IP.
344 function getRemoteIp()
346 if(!empty($this->env['HTTP_CLIENT_IP'])){
347 return $this->env['HTTP_CLIENT_IP'];
349 if(!empty($this->env['HTTP_X_FORWARDED_FOR'])){
350 foreach ((strstr($this->env['HTTP_X_FORWARDED_FOR'],',') ? split(',',$this->env['HTTP_X_FORWARDED_FOR']) : array($this->env['HTTP_X_FORWARDED_FOR'])) as $remote_ip){
351 if($remote_ip == 'unknown' ||
352 preg_match('/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/', $remote_ip) ||
353 preg_match('/^([0-9a-fA-F]{4}|0)(\:([0-9a-fA-F]{4}|0)){7}$/', $remote_ip)
355 return $remote_ip;
359 return empty($this->env['REMOTE_ADDR']) ? '' : $this->env['REMOTE_ADDR'];
364 * Returns the domain part of a host, such as akelos.com in 'www.akelos.com'. You can specify
365 * a different <tt>tld_length</tt>, such as 2 to catch akelos.co.uk in 'www.akelos.co.uk'.
367 function getDomain($tld_length = 1)
369 return preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$this->getHost()) ?
370 null :
371 join('.',array_slice(explode('.',$this->getHost()),(1 + $tld_length)*-1));
375 * Returns all the subdomains as an array, so ['dev', 'www'] would be returned for 'dev.www.akelos.com'.
376 * You can specify a different <tt>tld_length</tt>, such as 2 to catch ['www'] instead of ['www', 'akelos']
377 * in 'www.akelos.co.uk'.
379 function getSubdomains($tld_length = 1)
381 return preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$this->getHost()) ||
382 !strstr($this->getHost(),'.') ? array() : (array)array_slice(explode('.',$this->getHost()),0,(1 + $tld_length)*-1);
387 * Returns the request URI correctly
389 function getRequestUri()
391 return $this->getProtocol().$this->getHostWithPort();
395 * Return 'https://' if( this is an SSL request and 'http://' otherwise.
397 function getProtocol()
399 return $this->isSsl() ? 'https://' : 'http://';
403 * Is this an SSL request?
405 function isSsl()
407 return isset($this->env['HTTPS']) && ($this->env['HTTPS'] === true || $this->env['HTTPS'] == 'on');
411 * Returns the interpreted path to requested resource
413 function getPath()
415 return strstr($this->env['REQUEST_URI'],'?') ? substr($this->env['REQUEST_URI'],0,strpos($this->env['REQUEST_URI'],'?')) : $this->env['REQUEST_URI'];
419 * Returns the port number of this request as an integer.
421 function getPort()
423 $this->port_as_int = AK_WEB_REQUEST ? AK_SERVER_PORT : 80;
424 return $this->port_as_int;
428 * Returns the standard port number for this request's protocol
430 function getStandardPort()
432 return $this->isSsl() ? 443 : 80;
436 * Returns a port suffix like ':8080' if( the port number of this request
437 * is not the default HTTP port 80 or HTTPS port 443.
439 function getPortString()
441 $port = $this->getPort();
442 return $port == $this->getStandardPort() ? '' : ($port ? ':'.$this->getPort() : '');
446 * Returns a host:port string for this request, such as example.com or
447 * example.com:8080.
449 function getHostWithPort()
451 return $this->getHost() . $this->getPortString();
455 function getHost()
457 if(!empty($this->_host)){
458 return $this->_host;
460 return AK_WEB_REQUEST ? $this->env['SERVER_NAME'] : 'localhost';
463 function &getSession()
465 return $_SESSION;
468 function resetSession()
470 $_SESSION = array();
473 function &getCookies()
475 return $_COOKIE;
479 function &getEnv()
481 return $this->env;
485 function getServerSoftware()
487 if(!empty($this->env['SERVER_SOFTWARE'])){
488 if(preg_match('/^([a-zA-Z]+)/', $this->env['SERVER_SOFTWARE'],$match)){
489 return strtolower($match[0]);
492 return '';
497 * Returns true if the request's 'X-Requested-With' header contains
498 * 'XMLHttpRequest'. (The Prototype Javascript library sends this header with
499 * every Ajax request.)
501 function isXmlHttpRequest()
503 return !empty($this->env['HTTP_X_REQUESTED_WITH']) && strstr(strtolower($this->env['HTTP_X_REQUESTED_WITH']),'xmlhttprequest');
505 function xhr()
507 return $this->isXmlHttpRequest();
510 function isAjax()
512 return $this->isXmlHttpRequest();
517 * Receive the raw post data.
518 * This is useful for services such as REST, XMLRPC and SOAP
519 * which communicate over HTTP POST but don't use the traditional parameter format.
521 function getRawPost()
523 return empty($_ENV['RAW_POST_DATA']) ? '' : $_ENV['RAW_POST_DATA'];
527 function _validateTechName($name)
529 return preg_match('/^[A-Za-z]{1,}[A-Za-z0-9_]*$/',$name);
534 // {{{ _mergeRequest()
537 * Populates $this->_request attribute with incoming request in the following precedence:
539 * $_SESSION['request'] <- This will override options provided by previous methods
540 * $_COOKIE
541 * $_POST
542 * $_GET
543 * Command line params
545 * @access public
546 * @return void Void returned. Modifies the private property "
548 function _mergeRequest()
550 $this->_request = array();
552 $session_params = isset($_SESSION['request']) ? $_SESSION['request'] : null;
553 $command_line_params = !empty($_REQUEST) ? $_REQUEST : null;
555 $requests = array($command_line_params, $_GET, array_merge_recursive($_POST, $this->getPutParams(), $this->_getNormalizedFilesArray()), $_COOKIE, $session_params);
557 foreach ($requests as $request){
558 $this->_request = (!is_null($request) && is_array($request)) ?
559 array_merge($this->_request,$request) : $this->_request;
563 // }}}
565 function _getNormalizedFilesArray($params = null, $first_call = true)
567 $params = $first_call ? $_FILES : $params;
568 $result = array();
570 $params = array_diff($params,array(''));
571 if(!empty($params) && is_array($params)){
572 foreach ($params as $name=>$details){
574 if(is_array($details) && !empty($details['name']) && !empty($details['tmp_name']) && !empty($details['size'])){
575 if(is_array($details['tmp_name'])){
576 foreach ($details['tmp_name'] as $item=>$item_details){
577 if(is_array($item_details)){
578 foreach (array_keys($item_details) as $k){
579 if(UPLOAD_ERR_NO_FILE != $details['error'][$item][$k]){
580 $result[$name][$item][$k] = array(
581 'name'=>$details['name'][$item][$k],
582 'tmp_name'=>$details['tmp_name'][$item][$k],
583 'size'=>$details['size'][$item][$k],
584 'type'=>$details['type'][$item][$k],
585 'error'=>$details['error'][$item][$k],
589 }else{
590 if(UPLOAD_ERR_NO_FILE != $details['error'][$item]){
591 $result[$name][$item] = array(
592 'name'=>$details['name'][$item],
593 'tmp_name'=>$details['tmp_name'][$item],
594 'size'=>$details['size'][$item],
595 'type'=>$details['type'][$item],
596 'error'=>$details['error'][$item],
601 }elseif ($first_call){
602 $result[$name] = $details;
603 }else{
604 $result[$name][] = $details;
606 }elseif(is_array($details)){
607 $_nested = $this->_getNormalizedFilesArray($details, false);
609 if(!empty($_nested)){
610 $result = array_merge(array($name=>$_nested), $result);
616 return $result;
619 // {{{ _addParams()
622 * Builds (i.e., "expands") the Request class for accessing
623 * the request parameters as public properties.
624 * For example, when the requests is "ak=/controller/action/id&parameter=value",
625 * once parsed, you can access the parameters of the request just like
626 * an object, e.g.:
628 * $value_to_get = $request->parameter
630 * @access private
631 * @return void
633 function _addParam($variable, $value)
635 if($variable[0] != '_'){
636 if( ( $variable == 'action' && !$this->isValidActionName($value)) ||
637 ( $variable == 'controller' && !$this->isValidControllerName($value)) ||
638 ( $variable == 'module' && !$this->isValidModuleName($value))
640 return false;
642 $this->$variable = $value;
643 return true;
645 return false;
648 // }}}
652 * Correct double-escaping problems caused by "magic quotes" in some PHP
653 * installations.
655 function _fixGpcMagic()
657 if(!defined('AK_GPC_MAGIC_FIXED')){
658 if (get_magic_quotes_gpc()) {
659 array_walk($_GET, array('AkRequest', '_fixGpc'));
660 array_walk($_POST, array('AkRequest', '_fixGpc'));
661 array_walk($_COOKIE, array('AkRequest', '_fixGpc'));
663 define('AK_GPC_MAGIC_FIXED',true);
667 function _fixGpc(&$item)
669 if (is_array($item)) {
670 array_walk($item, array('AkRequest', '_fixGpc'));
671 }else {
672 $item = stripslashes($item);
677 function _urlDecode()
679 if(!defined('AK_URL_DECODED')){
680 array_walk($_GET, array('AkRequest', '_performUrlDecode'));
681 define('AK_URL_DECODED',true);
685 function _performUrlDecode(&$item)
687 if (is_array($item)) {
688 array_walk($item, array('AkRequest', '_performUrlDecode'));
689 }else {
690 $item = urldecode($item);
695 // {{{ recognize()
698 * Recognizes a Request and returns the responsible controller instance
700 * @return AkActionController
702 function &recognize($Map = null)
704 AK_ENVIRONMENT != 'setup' ? $this->_connectToDatabase() : null;
705 $this->_startSession();
706 $this->_enableInternationalizationSupport();
707 $this->_mapRoutes($Map);
709 $params = $this->getParams();
711 $module_path = $module_class_peffix = '';
712 if(!empty($params['module'])){
713 $module_path = trim(str_replace(array('/','\\'), DS, Ak::sanitize_include($params['module'], 'high')), DS).DS;
714 $module_shared_model = AK_CONTROLLERS_DIR.DS.trim($module_path,DS).'_controller.php';
715 $module_class_peffix = str_replace(' ','_',AkInflector::titleize(str_replace(DS,' ', trim($module_path, DS)))).'_';
718 $controller_file_name = AkInflector::underscore($params['controller']).'_controller.php';
719 $controller_class_name = $module_class_peffix.AkInflector::camelize($params['controller']).'Controller';
720 $controller_path = AK_CONTROLLERS_DIR.DS.$module_path.$controller_file_name;
721 include_once(AK_APP_DIR.DS.'application_controller.php');
723 if(!empty($module_path) && file_exists($module_shared_model)){
724 include_once($module_shared_model);
727 if(!is_file($controller_path) || !include_once($controller_path)){
728 defined('AK_LOG_EVENTS') && AK_LOG_EVENTS && $this->Logger->error('Controller '.$controller_path.' not found.');
729 if(AK_ENVIRONMENT == 'development'){
730 trigger_error(Ak::t('Could not find the file /app/controllers/<i>%controller_file_name</i> for '.
731 'the controller %controller_class_name',
732 array('%controller_file_name'=> $controller_file_name,
733 '%controller_class_name' => $controller_class_name)), E_USER_ERROR);
734 }elseif(@include(AK_PUBLIC_DIR.DS.'404.php')){
735 exit;
736 }else{
737 header("HTTP/1.1 404 Not Found");
738 die('404 Not found');
741 if(!class_exists($controller_class_name)){
742 defined('AK_LOG_EVENTS') && AK_LOG_EVENTS && $this->Logger->error('Controller '.$controller_path.' does not implement '.$controller_class_name.' class.');
743 if(AK_ENVIRONMENT == 'development'){
744 trigger_error(Ak::t('Controller <i>%controller_name</i> does not exist',
745 array('%controller_name' => $controller_class_name)), E_USER_ERROR);
746 }elseif(@include(AK_PUBLIC_DIR.DS.'405.php')){
747 exit;
748 }else{
749 header("HTTP/1.1 405 Method Not Allowed");
750 die('405 Method Not Allowed');
753 $Controller =& new $controller_class_name(array('controller'=>true));
754 $Controller->_module_path = $module_path;
755 isset($_SESSION) ? $Controller->session =& $_SESSION : null;
756 return $Controller;
760 // }}}
762 function _enableInternationalizationSupport()
764 if(AK_AVAILABLE_LOCALES != 'en'){
765 require_once(AK_LIB_DIR.DS.'AkLocaleManager.php');
767 $LocaleManager = new AkLocaleManager();
768 $LocaleManager->init();
769 $LocaleManager->initApplicationInternationalization($this);
770 $this->__internationalization_support_enabled = true;
774 function _mapRoutes($Map = null)
776 require_once(AK_LIB_DIR.DS.'AkRouter.php');
777 if(is_file(AK_ROUTES_MAPPING_FILE)){
778 if(empty($Map)){
779 $Map =& AkRouter();
781 include(AK_ROUTES_MAPPING_FILE);
782 // Set this routes for being used via Ak::toUrl
783 Ak::toUrl($Map,true);
784 $this->checkForRoutedRequests($Map);
789 function _connectToDatabase()
791 if(AK_AUTOMATIC_DB_CONNECTION){
792 Ak::db(AK_DEFAULT_DATABASE_PROFILE);
796 function _startSession()
798 if(AK_AUTOMATIC_SESSION_START){
799 if(!isset($_SESSION)){
800 if(AK_SESSION_HANDLER == 1 && defined('AK_DATABASE_CONNECTION_AVAILABLE') && AK_DATABASE_CONNECTION_AVAILABLE){
801 require_once(AK_LIB_DIR.DS.'AkDbSession.php');
803 $AkDbSession = new AkDbSession();
804 $AkDbSession->session_life = AK_SESSION_EXPIRE;
805 session_set_save_handler (
806 array(&$AkDbSession, '_open'),
807 array(&$AkDbSession, '_close'),
808 array(&$AkDbSession, '_read'),
809 array(&$AkDbSession, '_write'),
810 array(&$AkDbSession, '_destroy'),
811 array(&$AkDbSession, '_gc')
814 @session_start();
819 function getPutParams()
821 if(!isset($this->put) && $this->isPut() && $data = $this->getPutRequestData()){
822 $this->put = array();
823 parse_str(urldecode($data), $this->put);
825 return isset($this->put) ? $this->put : array();
828 function getPutRequestData()
830 if(!empty($_SERVER['CONTENT_LENGTH'])){
831 $putdata = fopen('php://input', 'r');
832 $result = fread($putdata, $_SERVER['CONTENT_LENGTH']);
833 fclose($putdata);
834 return $result;
835 }else{
836 return false;
841 function &AkRequest()
843 $null = null;
844 $AkRequest =& Ak::singleton('AkRequest', $null);
845 return $AkRequest;