-
Notifications
You must be signed in to change notification settings - Fork 0
Filters system
[h3] Introduction [/h3]
Filters system is similar to the servlet filters in Java or filters in Rails.
Filter is a class that is called right before calling the controller method and / or just after returning back from the controller.
It allows you for example:
- to redirect user before calling controller action if he is not logged in
- to implement access rights verification (similar to detecting users not logged in)
- to implement cache easily (check if cached version exists and return it before calling controller, store its results after returning from the controller)
- benchmarking (store action execution data into the database or send email to admin if execution seems to be slow)
Filter system is not limited to only one filter per request - you can configure as many filters as you like. The configuration of the filters is stored in system/application/config/filters.php. Filter classes should be placed in the system/filters (default filters provided with CI) and system/application/filters (application specific filters).
The filter system is implemented by 2 classes (system/library/CI_Pipe and Filter). CI_Pipe is filter manager - it does all the rules matching and runs filters specified in the configuration file.
[h3] Request execution flow [/h3]
The diagram below shows the way the request is executed in CI with the filters system.
Image:Request_processing_with_filters.png
[h3] Required changes in CodeIgniter.php [/h3]
To enable the filter system system/base/CodeIgniter.php had to be changed:
- lines 153-173 initialize filters system (load config and instantiate CI_Pipe)
- line 183 runs before() section of the filters
- line 200 runs after() section of the filters
[h3] Filter configuration file [/h3]
Example filter configuration file with some technical info (system/application/config/filter.php):
[code] <?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/* |
---|
FILTERS |
-------------------------------------------------------------------------- |
| | Filters allow you to execute arbitrary code before calling controller, | after its execution or even 'around' it - letting you take the complete | control over its execution. | | Example filters config: | | $filter['auth'] = array('exclude', array('login/', 'about/')); | $filter['cache'] = array('include', array('login/index', 'about/', 'register/form,rules,privacy')); | | Those lines define two filters - 'auth' and 'cache'. | | First parameter of the array containing filter definition determines the | way the following array is interpreted: | * 'exclude' - filter will be applied to all requests excluding those matching given patterns | * 'include' - filter will be applied only to the requests matching given patterns | | Next element is an array containing the list of controller and method patterns. | | You can use following patterns: | / - requests to the front page | * - all requests - use VERY carefully! | ctrl/ - requests to the controller 'ctrl' | ctrl/meth - requests to the method 'meth' of the controller 'ctrl' | ctrl/meth1,meth2 - requests to the methods 'meth1' and 'meth2' of the controller 'ctrl' | | The combination of 'exclude' and '*' means that the filter will be never applied. | | Interpretation of the example filters configuration: | | Filter 'auth' will be applied to each request excluding all requests to | the 'login' and 'about' controller. | | All requests which will pass the first filter will be processed by the | 'cache' filter, but only if they are 'login/index', any of the 'about' controller | methods or 'form', 'rules' or 'privacy' methods in the 'register' controller. | | | ------------------------------------------------------------------- | HOW IT WORKS | ------------------------------------------------------------------- | | First, CI will look for the 'auth.php' file containing filter definition | in the 'system/application/filters' directory. If it won't be found there | CI will look into 'system/filters' directory. As you can see, this is the | way to override default filters included in CI distribution. | | If slashes are used in the filter name, CI will search for the filter in | the giver directory. For example, if the filter name is 'security/auth', | CI will search for the 'auth.php' in the 'system/application/filters/security' | and in case of failure - in the 'system/filters/security' directory. | | Each filter can define one or both filtering methods - before() and after(). | | Method before() is executed just before calling controller action (and before | scaffolding code). | | Method after() is executed right after finishing controller action (but | before displaying any output) | | Remember that if you will use redirect() in the controller action or in the | before() filter method, the after() method will NOT be called. | | Example usages of the filters system: | - cacheing | - logging | - profiling | - authentication / authorization | */
/* |
---|
Filters configuration |
------------------------------------------------------------------- |
| | Note: The filters will be applied in the order that they are defined | | Example configuration: | | $filter['auth'] = array('exclude', array('login/', 'about/')); | $filter['cache'] = array('include', array('login/index', 'about/*', 'register/form,rules,privacy')); | */
$filter['auth'] = array('exclude', array('/', 'login/*')); //$filter['remove_xss'] = array('include', array('login/create')); ?> [/code]
[h3] Pipe class [/h3]
CI_Pipe class is the real filter engine. It processes filter configuration, matches request to the configured filters and provides means for the execution of filters' methods.
Code of the CI_Pipe class (system/library/Pipe.php):
[code] <?php if (!defined('BASEPATH')) exit('No direct script access allowed'); /** *
- Class provides request filtering feature for CI apps.
- CI_Pipe should be called after the execution of the request router,
- because it needs to know controller and method names which are
- going to be used to serve current request.
*/ class CI_Pipe { var $filters = array(); // filter configuration array var $call_stack = array(); // list of filter objects to call after processing the controller var $controller_name = ''; // name of the current request controller var $method_name = ''; // name of the current request action method
//-------------------------------------
// Initialize Prefs
//-------------------------------------
function initialize($config = array())
{
if (sizeof($config) > 0)
{
foreach ($config as $key => $val)
{
if (isset($this->$key))
{
$this->$key = $val;
}
}
}
}
// END
//-------------------------------------
// Default constructor - requires names of the requested controller and method.
//-------------------------------------
function __construct($filters, $controller_name, $method_name) {
$this->filters = $filters;
$this->controller_name = $controller_name;
$this->method_name = $method_name;
}
// END
//-------------------------------------
// Processes filter pipe before controller action call
//-------------------------------------
function process_before() {
foreach( $this->filters as $filter_name => $filter_conf ) {
if ( empty($filter_name) ) {
throw new Exception("Filter name can't be empty!");
}
if ($this->_applies($filter_conf)) {
$this->_run($filter_name);
}
}
}
// END
//-------------------------------------
// Processes filter pipe after controller action call
//-------------------------------------
function process_after() {
foreach( $this->call_stack as $filter ) {
$filter->after();
}
}
// END
//-------------------------------------
// PRIVATE: Tries to apply given filter config entry.
// Returns true if filter should be applied to the request and false in other case.
//-------------------------------------
function _applies($filter_conf)
{
$paths = $filter_conf[1];
switch ( $filter_conf[0] ) {
// exclusion mode
case 'exclude':
$apply = true;
foreach( $paths as $path ) {
if ( $this->_matches($path) ) {
return false;
}
}
break;
// inclusion mode
case 'include':
$apply = false;
foreach( $paths as $path ) {
if ( $this->_matches($path) ) {
return true;
}
}
break;
default:
throw new Exception('Bad type of filter type - only "exclude" and "include" are valid.');
}
return $apply;
}
// END
//-------------------------------------
// PRIVATE: Matches given URI pattern to the current request
//-------------------------------------
function _matches($path) {
if ( $path == '*' ) {
return true;
} else if ( $path == '/' ) {
if ($_SERVER['REQUEST_URI'] == '' || $_SERVER['REQUEST_URI'] == '/') {
return true;
}
} else {
$parts = explode('/', $path);
if ( $parts[1] == '*' ) {
if ( $parts[0] == $this->controller_name ) {
return true;
}
} else if ( strpos($parts[1], ',') !== false ) {
$subparts = explode( ',', $parts[1] );
if ( array_search($this->method_name, $subparts) !== false ) {
return true;
}
} else {
if ( $parts[0] == $this->controller_name && $parts[1] == $this->method_name ) {
return true;
}
}
}
return false;
}
// END
//-------------------------------------
// PRIVATE: Applies filter with given name
//-------------------------------------
function _run($filter_name) {
$this->_load_filter( $filter_name );
$class_name = $filter_name.'_filter';
$filter = new $class_name();
$this->call_stack[] = $filter;
$filter->before();
}
// END
//-------------------------------------
// PRIVATE: Locates and loads code of the filter with given name.
//-------------------------------------
function _load_filter($filter_name)
{
$file_name = $filter_name.EXT;
if ( file_exists(APPPATH.'filters/'.$file_name) ) {
require_once(APPPATH.'filters/'.$file_name);
} elseif ( file_exists(BASEPATH.'filters/'.$file_name) ) {
require_once(BASEPATH.'filters/'.$file_name);
} else {
throw new Exception("Filter [$filter_name] not found in application/filters nor in system/filters");
}
}
} ?> [/code]
[h3] Filter class [/h3]
Code of the Filter class (system/library/Filter.php):
[code] <?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/**
-
Base class for all filters. Defines empty before() and after() methods.
-
Override them in your filter class to customize the filter behavior. */ class Filter { function before() { }
function after() { } } ?> [/code]
[h3] Example filter [/h3]
Example filter from my app (system/application/filters/auth.php):
[code] <?php if (!defined('BASEPATH')) exit('No direct script access allowed'); /**
-
Authentication filter */ class Auth_filter extends Filter { function before() { $obj =& get_instance();
$obj->load->library('native_session'); $obj->load->helper('url');if (!$obj->session->userdata('user_id')) { // Character '/' is replaced by ':' because it causes problems // in URI interpretation and the URL encoded. Login controller // knows how to reverse this process and redirect to the given // URL. By using such solution, filter works even for query paths // turned off. $return_uri = urlencode( str_replace( '/', ':', substr( $_SERVER['REQUEST_URI'], 1 ) ) ); redirect('/login/index/'.$return_uri); }
}
} ?> [/code]
[h3] Excerpt from the changed CodeIgniter.php [/h3]
Changes in the system/codeigniter/CodeIgniter.php:
[code] // ...
require(BASEPATH.'libraries/Controller'.EXT); require(APPPATH.'controllers/'.$RTR->fetch_class().EXT);
/*
-
- Security check
-
- None of the functions in the app controller or the
- loader class can be called via the URI, nor can
- controller functions that begin with an underscore */ $class = $RTR->fetch_class(); $method = $RTR->fetch_method();
if ( ! class_exists($class) OR $method == 'controller' OR substr($method, 0, 1) == '_' OR in_array($method, get_class_methods('Controller')) ) { show_404(); }
/*
-
- Load the filters list file
-
*/ require(APPPATH.'config/filters'.EXT);
if ( ! isset($filter) OR ! is_array($filter)) { show_error('Your filters file does not appear to be formatted correctly.'); }
/*
-
- Initialize filters system
-
*/ require(BASEPATH.'libraries/Filter'.EXT); require(BASEPATH.'libraries/Pipe'.EXT);
$FPI = new CI_Pipe($filter, $class, $method);
/*
-
- Instantiate the controller and call requested method
-
*/ $CI = new $class();
// Run 'before' filters for the controller $FPI->process_before();
if ($RTR->scaffolding_request === TRUE) { $CI->_ci_scaffolding(); } else { if ( ! method_exists($CI, $method)) { show_404(); }
$CI->$method();
}
// Run 'after' filters for the controller $FPI->process_after();
/*
-
- Send the final rendered output to the browser
-
*/ $OUT->_display();
// ... [/code]