├── .travis.yml ├── README.md ├── Shield ├── Base.php ├── Bootstrap.php ├── Config.php ├── Di.php ├── Env.php ├── Exception.php ├── Filter.php ├── Input.php ├── Log.php ├── Session.php ├── Shield.php ├── View.php └── tests │ ├── Shield │ ├── ConfigTest.php │ ├── DiTest.php │ ├── InputTest.php │ ├── SessionTest.php │ ├── ShieldTest.php │ └── ViewTest.php │ ├── bootstrap.php │ └── phpunit.xml ├── app ├── .htaccess ├── config.php └── index.php └── composer.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | script: phpunit --configuration Shield/tests/phpunit.xml Shield/tests/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Shield : A Security-Minded Microframework 2 | =============== 3 | [![Build Status](https://secure.travis-ci.org/enygma/shieldframework.png?branch=master)](http://travis-ci.org/enygma/shieldframework) 4 | 5 | In my efforts to learn more about security best practices in PHP, I noticed that most of the PHP 6 | frameworks out there left it up to the developer to correctly handle input/output/etc themselves. 7 | Unfortunately, this has been a sticking point in PHP apps, so I decided to work on a microframework 8 | that was designed with security in mind. 9 | 10 | This project is under a MIT license. 11 | 12 | [shieldframework.com](http://shieldframework.com) 13 | 14 | Disclaimer 15 | ---------------- 16 | *Please note:* This framework is a work in progress and is serving as a resource to learn more 17 | about PHP and web application security. Use of this framework will *not* provide the perfect 18 | security for your application, nor should it be considered an ultimate resource for security 19 | best practices. 20 | 21 | ### Features: 22 | - Output filtering on all values (preventing XSS) 23 | - Logging on all actions 24 | - Input filtering functionality for accessing all superglobal information 25 | - Uses PHP's own filtering for data sanitization 26 | - Encrypted session handling (RIJNDAEL_256/MCRYPT_MODE_CBC, uses IV) 27 | - Custom cookie handling (including httpOnly) 28 | - Customized error handling to avoid exposing filesystem information 29 | - Basic templating/view system 30 | - IP-based access control 31 | - Session fixation prevention 32 | 33 | Requires 34 | --------------- 35 | * PHP 5.3.x 36 | * mcrypt extension (for sessions) 37 | 38 | The Code 39 | --------------- 40 | I'm a big fan of the Slim microframework, so anyone that's used that will feel at home with Shield. 41 | Here's some example code of it in use: 42 | 43 | ```php 44 | get('/',function(){ 49 | echo 'website root! woo!'; 50 | }); 51 | 52 | $app->run(); 53 | 54 | ``` 55 | 56 | The above example is super simple - all it does is handle (thanks to the included .htaccess file) 57 | the request for the root level route "/" as a GET request. When it matches the route, it executes 58 | the closure callback and echos out "website root! woo!" Easy right? 59 | 60 | Let's take a look at something a bit more complicated to introduce you to a few other handy tools 61 | at your disposal: 62 | 63 | ```php 64 | get('/',function() use ($app){ 69 | 70 | $app->filter->add('test','email'); 71 | 72 | echo 'from the URL: '.$app->input->get('test').'
'; 73 | 74 | $app->view->set('test','foodles'); 75 | return $app->view->render('index1 [test]'); 76 | }); 77 | 78 | $app->run(); 79 | 80 | ``` 81 | 82 | First off, there's one key difference between this example and the first one. In this example we 83 | pass in the `$app` object itself so we have access to some special features. Here's a quick overview: 84 | 85 | * `$app->view`: An instance of the View object that can be used to do some more complex view handling 86 | * `$app->filter`: A filtering object that lets you add and execute filters on the given data 87 | * `$app->input`: A feature to pull in values from the PHP superglobals ($_GET, $_POST, etc) 88 | * `$app->log`: A logging instance (what the framework uses too) 89 | * `$app->config`: Access to the configuration options, reading and writing 90 | 91 | There's also one other thing that could help in more complex development - the DI container. The framework 92 | makes heavy use of a Dependency Injection Container (DIC) to work with its resources. This is exposed 93 | back to the user as well, so you can access `$app->di` and use it to manage your own object instances as well. 94 | 95 | Regular Expression Routing 96 | ----------------- 97 | Besides the ability for Shield to match exact routes (like `/foo`), there's also a feature included allowing 98 | you use regular expresions in your routing. For example: 99 | 100 | ```php 101 | get('/foo([0-9]+)', function($matches) { 106 | print_r($matches); 107 | }); 108 | 109 | ``` 110 | 111 | Shield will try to match exact routes first, but then fall back on the regex routing checks. In th eabove example 112 | we're matching a route like `/foo123`. You'll notice that the first argument for the method is the routing matches as 113 | pulled from the [preg_match](http://php.net/preg_match) PHP method. You can use whatever preg-based expression you 114 | want to use and have the values returned to you in the `$matches` value. So: 115 | 116 | ```php 117 | get('/foo([0-9]+)', function($matches){ 122 | print_r($matches); 123 | }); 124 | ``` 125 | 126 | You would get `Array ( [1] => 123 )` in the `$matches` variable. 127 | 128 | You can also use named parameters in your routes too: 129 | 130 | ```php 131 | include_once '../Shield/Shield.php'; 132 | $app = new Shield\Shield(); 133 | 134 | $app->get('/params/[:t1]/[:t2]', function($matches){ 135 | return $app->view->render('First Parameter: '.$matches['t1']); 136 | }); 137 | ``` 138 | 139 | If your route is `/params/123/456` you'll get: 140 | 141 | `Array ( [t1] => 123, [t2] => 456 )` in the `$matches` variable. 142 | 143 | *NOTE:* DO NOT directly use the values from this array - there is currently no filtering on these values 144 | so there is potential for exploitation. 145 | 146 | Bound Configuration 147 | ----------------- 148 | You can also specify some configuration options linked directly to the route/closure combination. Here's an example: 149 | 150 | ```php 151 | get('/xml', function() use ($app){ 156 | return $app->view->render('this is xml'); 157 | }, array( 158 | 'view.content-type' => 'text/xml' 159 | )); 160 | ``` 161 | 162 | In the above example, we're overriding the `view.content-type` setting, but only for the `/` route, not everything. This gives us a bit more control over the application, making it easier to customize the request handling. Note this uses the dot notation to specify the value (the key). Most configuration options should be available for reconfiguration via this method. 163 | 164 | Documentation 165 | ----------------- 166 | ### Shield 167 | The `Shield` class is the main class you'll use and really only has a handful of methods: 168 | * `run()`: execute the application, no parameters 169 | * Each of the routing methods like `get()` and `post()`. Two parameters: route and closure/callback 170 | 171 | ### Config 172 | Access the values loaded from the configuration file or set/read your own. 173 | * `set($keyName,$value)`: Set a configuration value 174 | * `get($keyName)`: Get a configuration value 175 | * `load($path)`: Load the values from the path into the app (overwrites), default looks for "config.php" 176 | * `getConfig()`: Get all of the config options as an array 177 | * `setConfig($configArr)`: Set the array of options to the configuration (overwrites) 178 | 179 | ## Di 180 | Access to the dependency injection container (getting & setting) 181 | * `register($obj,$alias)`: Register an object in the container, `$alias` is optional. Uses classname as name 182 | if not defined 183 | * `get($name)`: Get the object with the given name from the container 184 | 185 | ### Filter 186 | Filter values based on filter types (supported are: email, striptags). Filters are applied when `get()` is called. 187 | * `add($fieldName,$type)`: Add a filter of the `$type` when the `$fieldName` is fetched 188 | * `filter($fieldName,$value)`: Looks for the filter(s) on the object and executes them in order (FIFO) on the `$value` 189 | 190 | *NOTE:* If no filters are specified, it will execute a "strip_tags" on the data by default. 191 | 192 | The `$type` parameter for the `add()` method can either be a string for the filter type or it can be a \Closure that will 193 | be given the value of the field as a parameter - for example: 194 | 195 | ```php 196 | filter->add('myField', function($value) { 199 | return 'returned: '.$value; 200 | }); 201 | ``` 202 | 203 | You must be sure to return from this closure, otherwise the filtering will return null. 204 | 205 | 206 | ### Input 207 | Pull values from the PHP superglobals (filtered) 208 | * `get($name)`: Pull from the $_GET, `$name` is name of variable 209 | * `post($name)`: Pull from the $_POST, `$name` is name of the variable 210 | * `request($name)`: Pull from the $_REQUEST, `$name` is name of the variable 211 | * `files($name)`: Pull from the $_FILES, `$name` is the name of the variable 212 | * `server($name)`: Pull from the $_SERVER, `$name` is the name of the variable 213 | * `set($type,$name,$value)`: Push a `$value` into the property `$name` of `$type` ('session','get','post',etc) 214 | 215 | *NOTE:* Superglobals are *unset* following a creation of an Input object. 216 | 217 | ### Log 218 | Logging to a file 219 | * `log($msg,$level)`: Message to log to the file, `$level` is optional (default "info") 220 | 221 | ### View 222 | Handle output to the page 223 | * `set($name,$value)`: Sets a variable into the view to be replaced in a template 224 | * `render($content)`: Renders and returns the content, any variables set to the object are replaced using the notation "[name]" 225 | 226 | *NOTE:* All values are escaped/filtered by default to prevent XSS. This can be overridden if desired. 227 | 228 | ### Template 229 | A basic templating engine included in the framework. By default it looks for a file named with the string given (in views/) or falls back to a `str_replace` method treating it as a string. 230 | * `render($template)`: Either the name of the template file (no .php) or the string to use as a template 231 | 232 | *NOTE:* If you choose to use the string as a template (no file), you must use the "[varName]" notation to get the values to substitute. Values can be set directly to the template instance (ex. `$app->view->template->test = 'foo';`) 233 | 234 | Configuration 235 | -------------- 236 | An optional `config.php` file can be placed in the same root as your front controller (probably `index.php`) so 237 | it can be found by the framework. This configuration file is a PHP array returned with your settings. These values 238 | can be accessed through the `$di->get('Config')->get()` method call. Here's an example config: 239 | 240 | ```php 241 | '/tmp' 244 | ); 245 | ``` 246 | 247 | Additionally, you can use a "dotted notation" to find configuration options. So, for example, to find the value below: 248 | 249 | ```php 250 | array( 253 | 'bar' => array( 254 | 'baz' => 'testing this' 255 | ) 256 | ) 257 | ); 258 | ``` 259 | 260 | You can use `$app->config->get('foo.bar.baz');` to get the value "testing this". 261 | 262 | ### Available Config options 263 | * `log_path`: Set the default logging path 264 | * `session.path`: Set the path on the local filesystem to save the session files to 265 | * `session.key`: Customize the key used for the session encryption 266 | * `session.lock`: Enable/disable session locking (binds session to the IP+User Agent to help prevent fixation) 267 | * `allowed_hosts`: Array of hosts allowed to make requests (whitelisting) 268 | * `force_https`: Allows you to force the use of HTTPS. Will redirect if enabled and HTTP is detected 269 | 270 | How To Contribute 271 | -------------- 272 | First off, thanks for considering submitting changes for the project - help is always appreciated! 273 | If you're going to contribute to the project, here's a few simple steps to follow: 274 | 275 | * When contributing, please make a branch on your clone of the repo and commit your changes there ( 276 | this makes it *much* simpler when the time comes to merge) 277 | * Submit a pull request with good detail on what changed - reading through code is fun, but a summary 278 | is better 279 | * Contact information is below - feel free to email or send a message on github if you have questions! 280 | 281 | Shield and the OWASP "Top Ten" 282 | -------------- 283 | One of the "gold standards" in the web application security community is the infamous ["Top Ten"](http://owasptop10.googlecode.com/files/OWASP%20Top%2010%20-%202010.pdf) list of common security issues that web apps have. Shield, being the nice framework that it is, tries to help protect you and your app from these problems. Here's how: 284 | 285 | * A1: Injection - All user input is filtered with at least one filter (including all PHP superglobals). 286 | * A2: Cross-Site Scripting - Before any information is accessed it is passed through at least one filter. Additionally, you can provide custom filtering via closures. 287 | * A3: Broken Authentication & Session Management - All session information is encrypted as it is stored using a Rijdael (256) method with an initialization vector. 288 | * A4: Insecure Direct Object References - Currently there's no permissioning system (and no auth system) in the framework. 289 | * A5: Cross-Site Request Forgery - Currently not prevented. 290 | * A6: Security Misconfiguration - The framework checks different PHP configuration settings to ensure that common security issues are mitigated. 291 | * A7: Insecure Cryptographic Storage - As previously mentioned, the only storage the framework does - sessions - stores the values encrypted. 292 | * A8: Failure to Restrict URL Access - Included in the framework is the ability to restrict based on IP. More fine-grained restriction is coming soon. 293 | * A9: Insufficient Transport Layer Protection - The framework currently does not prevent the use of HTTP over HTTPS. 294 | * A10: Unvalidated redirects & Forwards - The framework does not provide a mechanism for redirecting/forwarding. 295 | 296 | Contact 297 | -------------- 298 | Chris Cornutt 299 | 300 | [@enygma](http://twitter.com/enygma) 301 | 302 | 303 | -------------------------------------------------------------------------------- /Shield/Base.php: -------------------------------------------------------------------------------- 1 | di = $di; 22 | } 23 | 24 | /** 25 | * Throw a user error (NOTICE) with a given message 26 | * 27 | * @param string $msg Message 28 | * @param const $level Error level (from E_USER_* set) 29 | * 30 | * @return null 31 | */ 32 | protected function throwError($msg, $level=E_USER_WARNING) 33 | { 34 | trigger_error($msg, $level); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Shield/Bootstrap.php: -------------------------------------------------------------------------------- 1 | getMethods(); 13 | 14 | foreach ($methods as $method) { 15 | if (strstr($method->name, '_init') !== false) { 16 | $name = $method->name; 17 | $this->$name(); 18 | } 19 | } 20 | } 21 | 22 | private function _initEnvConfig() 23 | { 24 | error_reporting(-1); 25 | ini_set('display_errors', 1); 26 | ini_set('session.save_handler', 'files'); 27 | 28 | $contentType = Config::get('view.content-type'); 29 | $contentType = ($contentType == null) ? 'text/html' : $contentType; 30 | 31 | $charset = Config::get('view.charset'); 32 | $charset = ($charset == null) ? 'utf-8' : $charset; 33 | 34 | // render with the UTF-8 charset 35 | header('Content-Type: '.$contentType.'; charset='.$charset); 36 | } 37 | 38 | private function _initObjects() 39 | { 40 | // set up the custom encrypted session handler 41 | $session = new Session(); 42 | //$di->register($session); 43 | session_start(); 44 | 45 | // see if we need to lock our session 46 | $sessionLock = Config::get('session.lock'); 47 | if ($sessionLock == true) { 48 | $session->lock(); 49 | } 50 | 51 | // grab our input & filter 52 | $filter = new Filter(); 53 | $input = new Input($filter); 54 | 55 | session_set_cookie_params(3600, '/', $input->server('HTTP_HOST'), 1, true); 56 | //$di->register($input); 57 | 58 | $env = new Env($input); 59 | $env->check(); 60 | } 61 | } 62 | 63 | ?> -------------------------------------------------------------------------------- /Shield/Config.php: -------------------------------------------------------------------------------- 1 | array()); 12 | 13 | /** 14 | * Configuration file name (default) 15 | * @var string 16 | */ 17 | private static $configFile = 'config.php'; 18 | 19 | public function __construct() 20 | { 21 | //nothing to see... 22 | } 23 | 24 | /** 25 | * Load the configuration into the container (from a file) 26 | * 27 | * @param string $path Path to config file (an array returned) 28 | * 29 | * @throws \Exception If no config file or it's not a .php file 30 | * @return null 31 | */ 32 | public static function load($path=null) 33 | { 34 | if ($path == null) { 35 | $path = './'.self::$configFile; 36 | } 37 | $path = realpath($path); 38 | if (file_exists($path) && !is_readable($path)) { 39 | throw new \Exception('Cannot access configuration file!'); 40 | } 41 | 42 | if ($path !== false) { 43 | // be sure it's a .php file 44 | $info = pathinfo($path); 45 | 46 | if ($info['extension'] !== 'php') { 47 | throw new \Exception('File must be a .php file!'); 48 | } else { 49 | // we're good - load it! 50 | $data = include $path; 51 | self::setConfig($data); 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Set the values into the configuration container 58 | * 59 | * @param array $config Array of configuration options 60 | */ 61 | public static function setConfig($config,$context='general') 62 | { 63 | self::$config[$context] = $config; 64 | } 65 | 66 | /** 67 | * Set the filename to load config from 68 | * 69 | * @param string $fileName Name of file 70 | * 71 | * @return null 72 | */ 73 | public static function setConfigFile($fileName) 74 | { 75 | self::$configFile = $fileName; 76 | } 77 | 78 | /** 79 | * Get the current value for the config filename 80 | * 81 | * @return null 82 | */ 83 | public static function getConfigFile() 84 | { 85 | return self::$configFile; 86 | } 87 | 88 | /** 89 | * Get the full set of config options 90 | * 91 | * @return array Config container (options) 92 | */ 93 | public static function getConfig($context=null) 94 | { 95 | if ($context !== null && isset(self::$config[$context])) { 96 | return self::$config[$context]; 97 | } else { 98 | return self::$config; 99 | } 100 | } 101 | 102 | /** 103 | * Get a specific configuration option 104 | * 105 | * @param string $name Name of option 106 | * 107 | * @return mixed Either a string value or an array 108 | */ 109 | public static function get($name,$context='general') 110 | { 111 | if (strstr($name, '.') !== false) { 112 | // an array, split it and try to find it 113 | $parts = explode('.',$name); 114 | $current = self::$config[$context]; 115 | 116 | foreach ($parts as $p) { 117 | if (!isset($current[$p])) { return null; } 118 | $current = $current[$p]; 119 | } 120 | return $current; 121 | } else { 122 | // just a string 123 | return (isset(self::$config[$context][$name])) 124 | ? self::$config[$context][$name] : null; 125 | } 126 | } 127 | 128 | /** 129 | * Set a configuration opton 130 | * 131 | * @param string $name Name of option (will overwrite) 132 | * @param mixed $value Value to assign 133 | * 134 | * @return object Shield\Config 135 | */ 136 | public static function set($name,$value,$context='general') 137 | { 138 | self::$config[$context][$name] = $value; 139 | } 140 | 141 | /** 142 | * Use the given config and make updates to the context's settings 143 | * 144 | * @param array $config Array of configuration settings 145 | * @param string $context Context for the config options 146 | * 147 | * @return null 148 | */ 149 | public static function update($config,$context='general') 150 | { 151 | foreach ($config as $option => $value) { 152 | if (strstr($option, '.') !== false) { 153 | $opt = explode('.',$option); 154 | } else { 155 | $opt = array($option); 156 | } 157 | self::recurseConfig($opt,$value,self::$config[$context]); 158 | } 159 | } 160 | 161 | /** 162 | * Recurse through the configuration to apply the new values 163 | * 164 | * @param array $opt New config option path 165 | * @param mixed $value New value to set 166 | * @param array $config Current configuration options 167 | * 168 | * @return null 169 | */ 170 | private static function recurseConfig($opt,$value,&$config) 171 | { 172 | $first = array_shift($opt); 173 | if (isset($config[$first])) { 174 | if (count($opt) == 0) { 175 | $config[$first] = $value; 176 | } else { 177 | self::recurseConfig($opt,$value,$config[$first]); 178 | } 179 | } 180 | 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Shield/Di.php: -------------------------------------------------------------------------------- 1 | createInstance($i, $alias); 37 | } 38 | } 39 | 40 | /** 41 | * Get an object from the container 42 | * 43 | * @param string $name Name of the object 44 | * 45 | * @return mixed Either the object found or null 46 | */ 47 | public function get($name, $autoInit=false) 48 | { 49 | if (array_key_exists($name, $this->objects)) { 50 | return $this->objects[$name]; 51 | } else { 52 | if ($autoInit == true) { 53 | $this->createInstance($name); 54 | return $this->objects[$name]; 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Use the magic method to see if an object's in our set 61 | * 62 | * @param string $name Object name 63 | * 64 | * @return object 65 | */ 66 | public function __get($name) 67 | { 68 | return $this->get($name); 69 | } 70 | 71 | private function createInstance($instance, $alias=null) 72 | { 73 | if (!is_object($instance)) { 74 | // it's a string, make an object 75 | $instance = __NAMESPACE__.'\\'.$instance; 76 | $instance = new $instance($this); 77 | } 78 | 79 | $className = ($alias !== null) 80 | ? $alias : str_replace(__NAMESPACE__.'\\', '', get_class($instance)); 81 | 82 | if (!array_key_exists($className, $this->objects)) { 83 | $this->objects[$className] = $instance; 84 | return true; 85 | } else { 86 | return false; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Shield/Env.php: -------------------------------------------------------------------------------- 1 | input = $input; 12 | } 13 | 14 | /** 15 | * Execute the checks for various environment issues 16 | * 17 | * @return null 18 | */ 19 | public function check() 20 | { 21 | $this->checkHttps(); 22 | $this->setFrameHeader(); 23 | $this->checkRegisterGlobals(); 24 | $this->checkMagicQuotes(); 25 | } 26 | 27 | /** 28 | * Set a X-Frame-Options header 29 | * 30 | * @return null 31 | */ 32 | private function setFrameHeader() 33 | { 34 | header('X-Frame-Options: deny'); 35 | } 36 | 37 | /** 38 | * Check to see if we need to move to HTTPS by default 39 | * 40 | * @return null 41 | */ 42 | private function checkHttps() 43 | { 44 | // see if we need to be on HTTPS 45 | $httpsCfg = Config::get('force_https'); 46 | $httpsSet = $this->input->server('HTTPS'); 47 | 48 | if ($httpsCfg == true && empty($httpsSet)) { 49 | $host = $this->input->server('HTTP_HOST'); 50 | $request = $this->input->server('REQUEST_URI'); 51 | 52 | $redirect= "https://".$host.$request; 53 | header("Location: $redirect"); 54 | } 55 | } 56 | 57 | /** 58 | * Check the register_globals setting 59 | * 60 | * @return boolean Enabled/not enabled 61 | */ 62 | private function checkRegisterGlobals() 63 | { 64 | if (ini_get('register_globals')) { 65 | $this->throwError('SECURITY WARNING: register_globals is enabled! ' 66 | .'Please consider disabling.'); 67 | } else { 68 | return true; 69 | } 70 | } 71 | 72 | /** 73 | * Check the magic_quotes setting 74 | * 75 | * @return boolean Enabled/not enabled 76 | */ 77 | private function checkMagicQuotes() 78 | { 79 | if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) { 80 | $this->throwError('SECURITY WARNING: magic_quotes is enabled! ' 81 | .'Please consider disabling'); 82 | } else { 83 | return true; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Shield/Exception.php: -------------------------------------------------------------------------------- 1 | register($config); 16 | $log = new Log($config); 17 | 18 | $log->log($message); 19 | 20 | 21 | parent::__construct($message, $code, $previous); 22 | } 23 | 24 | /** 25 | * Converting the exception into a string to use for logging 26 | * 27 | * @return string Compiled exception string 28 | */ 29 | public function __toString() 30 | { 31 | return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; 32 | } 33 | } 34 | 35 | class RoutingException extends ShieldException 36 | { 37 | public function __construct($message, $code = 0, Exception $previous = null) { 38 | // nothing to see, move along 39 | parent::__construct($message, $code, $previous); 40 | } 41 | } 42 | 43 | ?> -------------------------------------------------------------------------------- /Shield/Filter.php: -------------------------------------------------------------------------------- 1 | $type); 36 | } 37 | 38 | foreach ($name as $n => $type) { 39 | if ($type == null) { continue; } 40 | 41 | if (isset($this->filters[$n])) { 42 | $this->filters[$n][] = $type; 43 | } else { 44 | $this->filters[$n] = array($type); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Get the filter(s) for a certain value 51 | * 52 | * @param string $name Field name 53 | * 54 | * @return string $func Function name 55 | */ 56 | public function get($name) 57 | { 58 | $func = array(); 59 | if (isset($this->filters[$name])) { 60 | foreach ($this->filters[$name] as $filter) { 61 | $func[] = $filter; 62 | } 63 | } 64 | return $func; 65 | } 66 | 67 | /** 68 | * Filter the given property (name) with the given value 69 | * 70 | * @param string $name Name of property 71 | * @param mixed $value Value to filter 72 | * 73 | * @return mixed $value Filtered value 74 | */ 75 | public function filter($name, $value) 76 | { 77 | $filters = $this->get($name); 78 | 79 | if (count($filters) == 0) { 80 | if ($value == null && is_string($value)) { 81 | $charset = Config::get('view.charset'); 82 | $encoding = ($charset !== null) ? $charset : 'UTF-8'; 83 | $value = htmlentities($value, ENT_QUOTES, $encoding, false); 84 | } 85 | } 86 | foreach ($filters as $filter) { 87 | if ($filter instanceof \Closure) { 88 | $value = $filter($value); 89 | } else { 90 | $func = 'filter'.ucwords(strtolower($filter)); 91 | if (method_exists($this, $func)) { 92 | $value = $this->$func($value); 93 | } 94 | } 95 | } 96 | return $value; 97 | } 98 | 99 | // ----------------- 100 | /** 101 | * Filter the value as if it was an email 102 | * 103 | * @param string $value Email value 104 | * 105 | * @return mixed Either te value if it matches or NULL 106 | */ 107 | private function filterEmail($value) 108 | { 109 | $val = filter_var($value, FILTER_VALIDATE_EMAIL); 110 | return ($val === $value) ? $val : null; 111 | } 112 | 113 | /** 114 | * Strip tags from the value 115 | * 116 | * @param string $value Value to filter 117 | * 118 | * @return string Filtered string 119 | */ 120 | private function filterStriptags($value) 121 | { 122 | return filter_var($value, FILTER_SANITIZE_STRING); 123 | } 124 | 125 | /** 126 | * Apply the htmlentities method on the value 127 | * 128 | * @param string $value Value to filter 129 | * 130 | * @return string Filtered result 131 | */ 132 | private function filterHtmlentities($value) 133 | { 134 | return htmlentities($value); 135 | } 136 | 137 | /** 138 | * Filter the value to see if it's an integer 139 | * 140 | * @param string $value Value to be filtered 141 | * 142 | * @return mixed Either the value or NULL 143 | */ 144 | private function filterInteger($value) 145 | { 146 | $val = filter_var($value, FILTER_VALIDATE_INT); 147 | return ($val == $value) ? $val : null; 148 | } 149 | 150 | /** 151 | * Filter the value to see if it's a value URL 152 | * 153 | * @param string $value Value to filter 154 | * 155 | * @return mixed Either the value or NULL 156 | */ 157 | private function filterUrl($value) 158 | { 159 | $val = filter_var($value, FILTER_VALIDATE_URL); 160 | return ($val == $value) ? $val : null; 161 | } 162 | 163 | /** 164 | * Filter the value to see if it's all lowercase 165 | * 166 | * @param string $value Value to filter 167 | * 168 | * @return mixed Either the value or NULL 169 | */ 170 | private function filterLowercase($value) 171 | { 172 | $val = strtolower($value); 173 | return ($val === $value) ? $val : null; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Shield/Input.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 55 | $this->load(); 56 | } 57 | 58 | /** 59 | * Load the values into the input object 60 | * Pulls in values from superglobals 61 | * 62 | * @return null 63 | */ 64 | public function load() 65 | { 66 | $data = array( 67 | 'get' => (isset($_GET)) ? $_GET : null, 68 | 'post' => (isset($_POST)) ? $_POST : null, 69 | 'request' => (isset($_REQUEST)) ? $_REQUEST : null, 70 | 'files' => (isset($_FILES)) ? $_FILES : null, 71 | 'server' => (isset($_SERVER)) ? $_SERVER : null, 72 | 'session' => (isset($_SESSION)) ? $_SESSION : null 73 | ); 74 | 75 | foreach ($data as $name => $value) { 76 | if ($value !== null) { 77 | $this->$name = $value; 78 | $global = strtoupper($name); 79 | unset($$global); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Run the filters associated with the property name 86 | * 87 | * @param string $name Property name 88 | * @param string $value Property value 89 | * 90 | * @return mixed Either null if the value is (false|null) or the actual value 91 | */ 92 | private function filterInput($name, $value) 93 | { 94 | if ($this->filter == null) { 95 | throw new \Exception('No filter object defined!'); 96 | } 97 | 98 | $val = $this->filter->filter($name, $value); 99 | return ($val !== false && $val !== null) ? $val : null; 100 | } 101 | 102 | /** 103 | * Set a value into the input type 104 | * 105 | * @param string $type Variable type 106 | * @param string $name Variable name 107 | * @param mixed $value Valiable value 108 | * 109 | * @return object Shield\Input instance 110 | */ 111 | public function set($type, $name, $value) 112 | { 113 | $type = strtolower($type); 114 | 115 | if (property_exists($this, $type)) { 116 | if (!is_array($this->$type)) { 117 | $this->$type = array(); 118 | } 119 | $arr = &$this->$type; 120 | $arr[$name] = $value; 121 | 122 | //sessions are special 123 | if ($type == 'session') { 124 | $_SESSION[$name] = $value; 125 | } 126 | } 127 | return $this; 128 | } 129 | 130 | /** 131 | * Get all if the RAW values of the given type (GET, POST, etc.) 132 | * 133 | * @param string $type Global type 134 | * 135 | * @return mixed Either the array of values or NULL 136 | */ 137 | public function getAll($type) 138 | { 139 | $type = strtolower($type); 140 | return (isset($this->$type)) ? $this->$type : null; 141 | } 142 | 143 | /** 144 | * Pull an information from the $_GET values 145 | * 146 | * @param string $name Name of the parameter 147 | * @param boolean $escape Escape the output (false is NOT recommended) 148 | * 149 | * @return mixed Either NULL or the value 150 | */ 151 | public function get($name, $escape=true) 152 | { 153 | if (isset($this->get[$name])) { 154 | if ($escape === true) { 155 | return $this->filterInput($name, $this->get[$name]); 156 | } else { 157 | $this->throwError( 158 | 'You are using the raw GET value of "'.$name.'"! Use with caution!' 159 | ); 160 | return $this->get[$name]; 161 | } 162 | } else { 163 | return null; 164 | } 165 | } 166 | 167 | /** 168 | * Pull a value from the $_POST values 169 | * 170 | * @param string $name Name of the variable 171 | * 172 | * @return mixed Found value or NULL 173 | */ 174 | public function post($name,$escape=true) 175 | { 176 | if (isset($this->post[$name])) { 177 | if ($escape === true) { 178 | return $this->filterInput($name, $this->post[$name]); 179 | } else { 180 | $this->throwError( 181 | 'You are using the raw POST value of "'.$name.'"! Use with caution!' 182 | ); 183 | return $this->post[$name]; 184 | } 185 | } else { 186 | return null; 187 | } 188 | } 189 | 190 | /** 191 | * Pull a value from the $_REQUEST values 192 | * 193 | * @param string $name Name of the variable 194 | * 195 | * @return mixed Found value or NULL 196 | */ 197 | public function request($name,$escape=true) 198 | { 199 | if (isset($this->request[$name])) { 200 | if ($escape === true) { 201 | return $this->filterInput($name, $this->request[$name]); 202 | } else { 203 | $this->throwError( 204 | 'You are using the raw REQUEST value of "'.$name.'"! Use with caution!' 205 | ); 206 | return $this->request[$name]; 207 | } 208 | } else { 209 | return null; 210 | } 211 | } 212 | 213 | /** 214 | * Pull a value from the $_FILES values 215 | * @todo implement filtering 216 | * 217 | * @param string $name Name of the variable 218 | * 219 | * @return mixed Found value or NULL 220 | */ 221 | public function files($name) 222 | { 223 | return (isset($this->files[$name])) ? $this->files[$name] : null; 224 | } 225 | 226 | /** 227 | * Pull a value from the $_SERVER values 228 | * 229 | * @param string $name Name of the variable 230 | * 231 | * @return mixed Found value or NULL 232 | */ 233 | public function server($name,$escape=true) 234 | { 235 | if (isset($this->server[$name])) { 236 | if ($escape === true) { 237 | return $this->filterInput($name, $this->server[$name]); 238 | } else { 239 | $this->throwError( 240 | 'You are using the raw SERVER value of "'.$name.'"! Use with caution!' 241 | ); 242 | return $this->server[$name]; 243 | } 244 | } else { 245 | return null; 246 | } 247 | } 248 | 249 | /** 250 | * Pull a value from the $_SESSION values 251 | * 252 | * @param string $name Name of the variable 253 | * 254 | * @return mixed Found value or NULL 255 | */ 256 | public function session($name, $escape=true) 257 | { 258 | if (isset($this->session[$name]) || isset($_SESSION[$name])) { 259 | $data = (isset($this->session[$name])) ? $this->session[$name] : $_SESSION[$name]; 260 | 261 | if ($escape === true) { 262 | return $this->filterInput($name, $data); 263 | } else { 264 | $this->throwError( 265 | 'You are using the raw SESSION value of "'.$name.'"! Use with caution!' 266 | ); 267 | return $this->session[$name]; 268 | } 269 | } else { 270 | return null; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /Shield/Log.php: -------------------------------------------------------------------------------- 1 | throwError('Cannot set log path - path does not exist!'); 18 | } elseif (!is_writeable($path)) { 19 | $this->throwError('Cannot set log path - not writeable!'); 20 | } else { 21 | $this->logPath = $path; 22 | } 23 | } 24 | 25 | /** 26 | * Get the current logging path 27 | * 28 | * @return string 29 | */ 30 | public function getLogPath() 31 | { 32 | return $this->logPath; 33 | } 34 | 35 | /** 36 | * Make the default log path 37 | * 38 | * @return null 39 | */ 40 | public function makeLogPath($logPath=null) 41 | { 42 | $logPath = ($logPath !== null) ? $logPath : $this->logPath; 43 | 44 | // check to see if we can write to it 45 | if (is_writable($logPath)) { 46 | mkdir($logPath); 47 | return true; 48 | } else { 49 | $this->throwError('Cannot create logs/ directory'); 50 | return false; 51 | } 52 | } 53 | 54 | /** 55 | * Init the object and set up the config file path 56 | * 57 | * @param object $di DI container 58 | * 59 | * @return null 60 | */ 61 | public function __construct() 62 | { 63 | // check config for a path or set a default logging path 64 | $logPath = Config::get('log_path'); 65 | 66 | if ($logPath !== null && is_dir(realpath($logPath)) && is_writable($logPath)) { 67 | $this->setLogPath(realpath($logPath)); 68 | } else { 69 | $logPath = __DIR__.'/../app/../logs'; 70 | $realpath = realpath($logPath); 71 | 72 | if ($realpath === false) { 73 | // directory may not exist, try to create 74 | if ($this->makeLogPath($logPath) === true) { 75 | $this->setLogPath($logPath); 76 | } else { 77 | // all else fails, write to /tmp 78 | $this->setLogPath('/tmp'); 79 | } 80 | } else { 81 | $this->setLogPath($realpath); 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Log the message to the data source 88 | * 89 | * @param string $msg Message to write 90 | * 91 | * @return null 92 | */ 93 | public function log($msg, $level='info') 94 | { 95 | $logFile = $this->getLogPath().'/log-'.date('Ymd').'.log'; 96 | 97 | if (is_writeable($this->getLogPath())) { 98 | if (!$fp = fopen($logFile, 'a+')) { 99 | $this->throwError('Cannot open the log file.'); 100 | } 101 | if ($fp) { 102 | $msg = '['.date('m.d.Y H:i:s').'] ['.strtoupper($level).'] '.$msg; 103 | if (fwrite($fp, $msg."\n") === false) { 104 | $this->throwError('Cannot write to the log file.'); 105 | } 106 | fclose($fp); 107 | } 108 | } else { 109 | $this->throwError('Cannot write to the log directory.'); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Shield/Session.php: -------------------------------------------------------------------------------- 1 | key = $sessionKey; 46 | } 47 | $sessionPath = Config::get('session.path'); 48 | $this->savePathRoot = ($sessionPath == null) 49 | ? ini_get('session.save_path') : $sessionPath; 50 | 51 | //parent::__construct($di); 52 | } 53 | 54 | /** 55 | * If locking is on, check the values to ensure there's a match 56 | * 57 | * @return boolean Lock status 58 | */ 59 | public function lock() 60 | { 61 | $log = new Log(); 62 | $log->log('Session locking in effect'); 63 | 64 | $sip = (isset($_SESSION['sIP'])) ? $_SESSION['sIP'] : null; 65 | $sua = (isset($_SESSION['sUA'])) ? $_SESSION['sUA'] : null; 66 | 67 | // if they're null, set them 68 | if ($sip == null && $sua == null) { 69 | $_SESSION['sIP'] = $_SERVER['REMOTE_ADDR']; 70 | $_SESSION['sUA'] = $_SERVER['HTTP_USER_AGENT']; 71 | } elseif ($sip !== null && $sua !== null) { 72 | // see if we have a match, if not refresh() 73 | if ($sip !== $_SERVER['REMOTE_ADDR'] || $sua !== $_SERVER['HTTP_USER_AGENT']) { 74 | $log->log('SECURITY ALERT: Session lock override attempt!'); 75 | $this->refresh(); 76 | return false; 77 | } 78 | } 79 | return true; 80 | } 81 | 82 | /** 83 | * Encrypt the given data 84 | * 85 | * @param mixed $data Session data to encrypt 86 | * 87 | * @return mixed $data Encrypted data 88 | */ 89 | private function encrypt($data) 90 | { 91 | $ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); 92 | $iv = mcrypt_create_iv($ivSize, MCRYPT_RAND); 93 | $keySize = mcrypt_get_key_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); 94 | $key = substr(sha1($this->key), 0, $keySize); 95 | 96 | // add in our IV and base64 encode the data 97 | $data = base64_encode( 98 | $iv.mcrypt_encrypt( 99 | MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv 100 | ) 101 | ); 102 | return $data; 103 | } 104 | 105 | /** 106 | * Decrypt the given session data 107 | * 108 | * @param mixed $data Data to decrypt 109 | * 110 | * @return $data Decrypted data 111 | */ 112 | private function decrypt($data) 113 | { 114 | $data = base64_decode($data, true); 115 | 116 | $ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); 117 | $keySize = mcrypt_get_key_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); 118 | $key = substr(sha1($this->key), 0, $keySize); 119 | 120 | $iv = substr($data, 0, $ivSize); 121 | $data = substr($data, $ivSize); 122 | 123 | $data = mcrypt_decrypt( 124 | MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv 125 | ); 126 | 127 | return $data; 128 | } 129 | 130 | /** 131 | * Write to the session 132 | * 133 | * @param integer $id Session ID 134 | * @param mixed $data Data to write to the log 135 | * 136 | * @return null 137 | */ 138 | public function write($id, $data) 139 | { 140 | $path = $this->savePathRoot.'/shield_'.$id; 141 | $data = $this->encrypt($data); 142 | 143 | file_put_contents($path, $data); 144 | } 145 | 146 | /** 147 | * Set the key for the session encryption to use (default is set) 148 | * 149 | * @param string $key Key string 150 | * 151 | * @return null 152 | */ 153 | public function setKey($key) 154 | { 155 | $this->key = $key; 156 | } 157 | 158 | /** 159 | * Read in the session 160 | * 161 | * @param string $id Session ID 162 | * 163 | * @return null 164 | */ 165 | public function read($id) 166 | { 167 | $path = $this->savePathRoot.'/shield_'.$id; 168 | $data = null; 169 | 170 | if (is_file($path)) { 171 | // get the data and extract the IV 172 | $data = file_get_contents($path); 173 | $data = $this->decrypt($data); 174 | } 175 | 176 | return $data; 177 | } 178 | 179 | /** 180 | * Close the session 181 | * 182 | * @return boolean Default return (true) 183 | */ 184 | public function close() 185 | { 186 | return true; 187 | } 188 | 189 | /** 190 | * Perform garbage collection on the session 191 | * 192 | * @param int $maxlifetime Lifetime in seconds 193 | * 194 | * @return null 195 | */ 196 | public function gc($maxlifetime) 197 | { 198 | $path = $this->savePathRoot.'/shield_*'; 199 | 200 | foreach (glob($path) as $file) { 201 | if (filemtime($file) + $maxlifetime < time() && file_exists($file)) { 202 | unlink($file); 203 | } 204 | } 205 | 206 | return true; 207 | } 208 | 209 | /** 210 | * Open the session 211 | * 212 | * @param string $savePath Path to save the session file locally 213 | * @param string $sessionId Session ID 214 | * 215 | * @return null 216 | */ 217 | public function open($savePath, $sessionId) 218 | { 219 | // open session 220 | } 221 | 222 | /** 223 | * Destroy the session 224 | * 225 | * @param string $id Session ID 226 | * 227 | * @return null 228 | */ 229 | public function destroy($id) 230 | { 231 | $path = $this->savePathRoot.'/shield_'.$id; 232 | if (is_file($path)) { 233 | unlink($path); 234 | } 235 | return true; 236 | } 237 | 238 | /** 239 | * Refresh the session with a new ID 240 | * DO NOT repopulate the session information 241 | * 242 | * @return null 243 | */ 244 | public function refresh() 245 | { 246 | $sid = session_id(); 247 | if (!empty($sid)) { 248 | error_log('refreshing session!'); 249 | 250 | $id = session_regenerate_id(true); 251 | session_destroy(); 252 | session_start(); 253 | } 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /Shield/Shield.php: -------------------------------------------------------------------------------- 1 | 'Error', 38 | 2 => 'Warning', 39 | 4 => 'Parse error', 40 | 8 => 'Notice', 41 | 16 => 'Core Error', 42 | 32 => 'Core Warning', 43 | 256 => 'User Error', 44 | 512 => 'User Warning', 45 | 1024 => 'User Notice', 46 | 2048 => 'Strict', 47 | 4096 => 'Recoverable Error', 48 | 8192 => 'Deprecated', 49 | 16384 => 'User Deprecated', 50 | 32767 => 'All' 51 | ); 52 | 53 | /** 54 | * Init the object and its dependencies: 55 | * 56 | * Filter, Input, Session, Config 57 | * View, Log, Di (DI container) 58 | * 59 | * Also register the custom session handler (encrypted) 60 | * 61 | * @return null 62 | */ 63 | public function __construct() 64 | { 65 | // set the APPPATH constant 66 | if (!defined('APPPATH')) { 67 | define('APPPATH', __DIR__.'../app'); 68 | } 69 | 70 | // some global config 71 | spl_autoload_register(array($this, '_load')); 72 | set_error_handler(array($this, '_errorHandler')); 73 | set_exception_handler(array($this, '_exceptionHandler')); 74 | 75 | // include our exceptions 76 | include_once 'Exception.php'; 77 | 78 | $this->init(); 79 | } 80 | 81 | private function init() 82 | { 83 | // init the config and read it in (if it exists) 84 | Config::load(); 85 | 86 | $bs = new Bootstrap(); 87 | 88 | $this->filter = new Filter(); 89 | 90 | // set up the view and logger objects 91 | $this->log = new Log(); 92 | $this->input = new Input($this->filter); 93 | $this->view = new View($this->filter, $this->input); 94 | } 95 | 96 | /** 97 | * Handle unknown method calls (get() or post() - request methods) 98 | * 99 | * @param string $func Function name 100 | * @param mixed $args Arguments list 101 | * 102 | * @return null 103 | */ 104 | public function __call($func, $args) 105 | { 106 | $func = strtolower($func); 107 | $path = strtolower($args[0]); 108 | 109 | if (isset($args[2])) { 110 | // we've been given a route-specific config, set it up! 111 | Config::setConfig($args[2], 'route::'.$path); 112 | } 113 | 114 | if (isset($args[1])) { 115 | 116 | // see if the path includes params 117 | if (strpos($path, ':') !== false) { 118 | preg_match_all('/\[(.+?)\]/', $path, $params); 119 | 120 | // and replace the values in the path 121 | foreach ($params[1] as $p) { 122 | $path = str_replace('/['.$p.']','/(.+?)', $path); 123 | } 124 | $this->params[$func][$path] = $params[1]; 125 | } 126 | 127 | $this->routes[$func][$path] = $args[1]; 128 | 129 | $this->log->log('SETTING PATH ['.strtoupper($func).']: '.$path); 130 | } else { 131 | $this->throwError('No path to set for : '.strtoupper($func)); 132 | $this->log->log('NO PATH TO SET ['.strtoupper($func).']: '.$path); 133 | } 134 | } 135 | 136 | /** 137 | * PSR-0 Compliant Autoloader 138 | * 139 | * @param string $className Name of class to load (namespaced) 140 | * 141 | * @return null 142 | */ 143 | private function _load($className) 144 | { 145 | $path = __DIR__.'/'.str_replace('Shield\\', '/', $className).'.php'; 146 | if (is_file($path)) { 147 | include_once $path; 148 | return true; 149 | } else { 150 | return false; 151 | } 152 | } 153 | 154 | /** 155 | * Execute the request handling! 156 | * 157 | * @return null 158 | */ 159 | public function run() 160 | { 161 | $requestMethod = $this->input->server('REQUEST_METHOD'); 162 | $queryString = $this->input->server('QUERY_STRING'); 163 | $requestUri = $this->input->server('REQUEST_URI'); 164 | $remoteAddr = $this->input->server('REMOTE_ADDR'); 165 | 166 | // if we have the config option, see if they're allowed 167 | $allowedHosts = Config::get('allowed_hosts'); 168 | if (!empty($allowedHosts)) { 169 | if (!in_array($remoteAddr, $allowedHosts)) { 170 | // not allowed, fail! 171 | header('HTTP/1.0 401 Not Authorized'); 172 | return false; 173 | } 174 | } 175 | 176 | // try and match our route and request type 177 | $uri = strtolower(str_replace('?'.$queryString, '', $requestUri)); 178 | $method = strtolower($requestMethod); 179 | 180 | if (isset($this->routes[$method][$uri])) { 181 | 182 | $this->routeMatch($method, $uri, $uri); 183 | 184 | } else { 185 | $found = false; 186 | 187 | if (isset($this->routes[$method])) { 188 | 189 | // loop through our routes and see if there's a regex match 190 | foreach ($this->routes[$method] as $route => $handler) { 191 | if (preg_match('#^'.$route.'$#', $uri, $matches) === 1 && $found == false) { 192 | // drop the first value, it's the full URL 193 | $matches = array_values(array_slice($matches,1)); 194 | $found = true; 195 | $this->routeMatch($method, $route, $matches); 196 | } 197 | } 198 | 199 | if ($found == false) { 200 | // return a 404 header 201 | header('HTTP/1.0 404 Not Found'); 202 | 203 | $this->throwError('No route match for "'.$uri.'"'); 204 | throw new RoutingException('NO ROUTE MATCH ['.strtoupper($method).']: '.$uri); 205 | } 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * Handle the matching route callback 212 | * 213 | * @param string $method HTTP Method 214 | * @param string $uri URI/route to match 215 | * 216 | * @return null 217 | */ 218 | private function routeMatch($method,$uri,$matches=null) 219 | { 220 | // given our URI, see if we have a match in our Config & update! 221 | $config = Config::getConfig('route::'.$uri); 222 | if ($config !== null) { 223 | Config::update($config); 224 | } 225 | 226 | // route match! 227 | $this->log->log('ROUTE MATCH ['.strtoupper($method).']: '.$uri); 228 | 229 | //see if we have params to match the items to 230 | if ($matches !== null && isset($this->params[$method][$uri])) { 231 | $params = $this->params[$method][$uri]; 232 | 233 | // match them up 234 | $tmp = array(); 235 | foreach ($matches as $index => $match) { 236 | if (isset($params[$index])) { 237 | $param = str_replace(':','',$params[$index]); 238 | $tmp[$param] = $match; 239 | } 240 | } 241 | if (!empty($tmp)){ 242 | $matches = $tmp; 243 | } 244 | } 245 | 246 | $routeClosure = $this->routes[$method][$uri]($matches); 247 | $content = $this->view->render($routeClosure); 248 | echo $content; 249 | } 250 | 251 | /** 252 | * Throw a user error (NOTICE) with a given message 253 | * 254 | * @param string $msg Message 255 | * @param const $level Error level (from E_USER_* set) 256 | * 257 | * @return null 258 | */ 259 | protected function throwError($msg, $level=E_USER_WARNING) 260 | { 261 | trigger_error($msg, $level); 262 | } 263 | 264 | public function _errorHandler($errno, $errstr, $errfile, $errline) 265 | { 266 | $errString = (array_key_exists($errno, $this->errorConstants)) 267 | ? $this->errorConstants[$errno] : $errno; 268 | 269 | echo ''.$errString.': '.$errstr.'
'; 270 | 271 | error_log($errString.' ['.$errno.']: '.$errstr.' in '.$errfile.' on line '.$errline); 272 | } 273 | 274 | /** 275 | * Custom exception handler 276 | * 277 | * @param Exception 278 | * 279 | * @return null 280 | */ 281 | public function _exceptionHandler($exception) 282 | { 283 | $message = get_class($exception).' - '.$exception->getMessage().' [code: '.$exception->getCode().']'; 284 | $this->log->log($message); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Shield/View.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 15 | $this->setViewDir(); 16 | } 17 | 18 | /** 19 | * Template properties (values) 20 | * @var array 21 | */ 22 | private $_properties = array(); 23 | 24 | /** 25 | * Magic method to get values from the properties 26 | * 27 | * @param string $name Name of property 28 | * 29 | * @return null 30 | */ 31 | public function __get($name) 32 | { 33 | return (isset($this->_properties[$name])) 34 | ? $this->_properties[$name] : null; 35 | } 36 | 37 | /** 38 | * Magic method to set values to the properties 39 | * 40 | * @param string $name Property name 41 | * @param mixed $value Property value 42 | * 43 | * @return null description 44 | */ 45 | public function __set($name,$value) 46 | { 47 | $this->set($name, $value); 48 | } 49 | 50 | /** 51 | * Set the value(s) to the template 52 | * 53 | * @param string $index Property name 54 | * @param mixed $value Property value 55 | * 56 | * @return object $this Current View instance 57 | */ 58 | public function set($index, $value=null) 59 | { 60 | if (!is_array($index)) { 61 | $index = array($index => $value); 62 | } 63 | foreach ($index as $i => $value) { 64 | $this->_properties[$i] = $value; 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Add a filter to a property 72 | * 73 | * @param string $index Property name 74 | * @param mixed $filter Either a filter type or a closure 75 | * 76 | * @return object $this View instance 77 | */ 78 | public function filter($index, $filter=null) 79 | { 80 | if (!is_array($index)) { 81 | $index = array($index => $filter); 82 | } 83 | foreach ($index as $i => $value) { 84 | if (is_array($value)) { 85 | foreach ($value as $f) { 86 | $this->filter->add($i, $f); 87 | } 88 | } else { 89 | $this->filter->add($i, $filter); 90 | } 91 | } 92 | return $this; 93 | } 94 | 95 | /** 96 | * Get a value from our properties 97 | * 98 | * @param string $index Index name of property 99 | * 100 | * @return mixed Either NULL or the property value, if found 101 | */ 102 | public function get($index) 103 | { 104 | return (isset($this->_properties[$index])) ? $this->_properties[$index] : null; 105 | } 106 | 107 | /** 108 | * Get the current templates directory 109 | * 110 | * @return string Full path to templates directory 111 | */ 112 | public function getViewDir() 113 | { 114 | return $this->templateDir; 115 | } 116 | 117 | /** 118 | * Set the directory where the templates live 119 | * 120 | * @param string $dir Directory path 121 | * 122 | * @return null 123 | */ 124 | public function setViewDir($dir=null) 125 | { 126 | // see if the path is valid 127 | $templatePath = ($dir !== null) ? $dir : __DIR__.'/../app/views'; 128 | 129 | if (realpath($templatePath) !== false) { 130 | $this->templateDir = realpath($templatePath); 131 | } 132 | return $this; 133 | } 134 | 135 | /** 136 | * Get the current content type value 137 | * 138 | * @return string Content type 139 | */ 140 | public function getContentType() 141 | { 142 | $contentType = Config::get('view.content-type'); 143 | return ($contentType !== null) ? $contentType : $this->contentType; 144 | } 145 | 146 | /** 147 | * Set the content type (ex. "text/html") 148 | * 149 | * @param string $contentType Content type string 150 | * 151 | * @return object $this Template 152 | */ 153 | public function setContentType($contentType) 154 | { 155 | $this->contentType = $contentType; 156 | return $this; 157 | } 158 | 159 | /** 160 | * Get the current character set 161 | * 162 | * @return string Character set 163 | */ 164 | public function getCharset() 165 | { 166 | $charset = Config::get('view.charset'); 167 | return ($charset !== null) ? $charset : $this->charset; 168 | } 169 | 170 | /** 171 | * Set the character set 172 | * 173 | * @param string $charset Character set (ex. "UTF-8") 174 | */ 175 | public function setCharset($charset) 176 | { 177 | $this->charset = $charset; 178 | return $this; 179 | } 180 | 181 | /** 182 | * Render the template - either using a file (views/) or as a string 183 | * Checks to see if the $template references a file first 184 | * 185 | * @param string $template Either a filename (views/) or a string 186 | * 187 | * @return string Rendered output 188 | */ 189 | public function render($template) 190 | { 191 | $charset = $this->getCharset(); 192 | $contentType = $this->getContentType(); 193 | 194 | header('Content-Type: '.$contentType.'; charset='.$charset); 195 | 196 | // first see if what we've been given is a file 197 | $templateFile = $this->getViewDir().'/'.$template.'.php'; 198 | 199 | // run through our properties and filter 200 | foreach ($this->_properties as $index => $value) { 201 | $this->_properties[$index] = $this->filter->filter($index,$value); 202 | } 203 | 204 | if (is_file($templateFile)) { 205 | 206 | // scope in the tempate extraction 207 | $result = function($file,array $data=array()) { 208 | ob_start(); 209 | extract($data, EXTR_SKIP); 210 | try { 211 | include $file; 212 | } catch(\Exception $e) { 213 | ob_end_clean(); 214 | throw $e; 215 | } 216 | return ob_get_clean(); 217 | }; 218 | 219 | return $result($templateFile, $this->_properties); 220 | 221 | } else { 222 | // it's just a string! fall back on str_replace 223 | foreach ($this->_properties as $name => $value) { 224 | $template = str_replace('['.$name.']', $value, $template); 225 | } 226 | 227 | // replace any leftovers 228 | $template = preg_replace('#\[.*?\]#', '', $template); 229 | return $template; 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Shield/tests/Shield/ConfigTest.php: -------------------------------------------------------------------------------- 1 | _di = new Di(); 13 | //$this->_config = new Config($this->_di); 14 | } 15 | public function tearDown() 16 | { 17 | $this->_di = null; 18 | //$this->_config = null; 19 | } 20 | 21 | /** 22 | * Test that setting/getting a configuration value works 23 | * 24 | * @return null 25 | */ 26 | public function testSetGetValue() 27 | { 28 | Config::set('testing','foo123'); 29 | $this->assertEquals( 30 | Config::get('testing'),'foo123' 31 | ); 32 | } 33 | 34 | /** 35 | * Test that the default configuration filename is set 36 | * 37 | * @return null 38 | */ 39 | public function testGetDefaultConfigFilename() 40 | { 41 | $filename = Config::getConfigFile(); 42 | $this->assertEquals($filename,'config.php'); 43 | } 44 | 45 | /** 46 | * Test that the configuration filename value is set correctly 47 | * 48 | * @return null 49 | */ 50 | public function testSetConfigFilename() 51 | { 52 | $originalConfig = Config::getConfigFile(); 53 | $newFilename = 'testconfig.php'; 54 | 55 | Config::setConfigFile($newFilename); 56 | 57 | $this->assertEquals( 58 | $newFilename, 59 | Config::getConfigFile() 60 | ); 61 | 62 | // reset it back to the original 63 | Config::setConfigFile($originalConfig); 64 | } 65 | 66 | /** 67 | * Test that the custom configuration can be set correctly 68 | * 69 | * @return null 70 | */ 71 | public function testSetCustomConfiguration() 72 | { 73 | $option = 'foo123'; 74 | $config = array('test' => $option); 75 | 76 | Config::setConfig($config); 77 | 78 | $this->assertEquals( 79 | Config::get('test'), 80 | $option 81 | ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Shield/tests/Shield/DiTest.php: -------------------------------------------------------------------------------- 1 | _di = new Di(); 12 | } 13 | public function tearDown() 14 | { 15 | $this->_di = null; 16 | } 17 | 18 | /** 19 | * Test that an object is correctly injected into the DI container 20 | * 21 | * @return null 22 | */ 23 | public function testObjectRegister() 24 | { 25 | $obj = new \stdClass(); 26 | $obj->testing = 'foo'; 27 | 28 | $this->_di->register($obj); 29 | $ret = $this->_di->get('stdClass'); 30 | 31 | $this->assertTrue(isset($ret->testing) && $ret->testing == 'foo'); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Shield/tests/Shield/InputTest.php: -------------------------------------------------------------------------------- 1 | _di = new Di(); 20 | $this->_input = new Input(new Filter()); 21 | //$this->_config = new Config($this->_di); 22 | } 23 | public function tearDown() 24 | { 25 | $this->_di = null; 26 | $this->_input = null; 27 | } 28 | 29 | /** 30 | * Test that, when a Input object has a Filter object set on it 31 | * that the get() call correctly filters (in this case, an email address) 32 | * 33 | * @return null 34 | */ 35 | public function testValidInputIsFiltered() 36 | { 37 | global $_GET; 38 | global $_POST; 39 | global $_REQUEST; 40 | global $_FILES; 41 | global $_SERVER; 42 | 43 | $validEmail = 'woo@test.com'; 44 | 45 | // create and register a Filter instance 46 | $filter = new Filter(); 47 | $filter->add('testVal','email'); 48 | 49 | $input = new Input($filter); 50 | $input->set('get','testVal',$validEmail); 51 | 52 | $result = $input->get('testVal'); 53 | $this->assertEquals($result,$validEmail); 54 | } 55 | 56 | /** 57 | * Test using a closure as a filter 58 | * 59 | * @return null 60 | */ 61 | public function testFilterAsClosure() 62 | { 63 | global $_GET; 64 | global $_POST; 65 | global $_REQUEST; 66 | global $_FILES; 67 | global $_SERVER; 68 | 69 | $validEmail = 'woo@test.com'; 70 | 71 | // create and register a Filter instance 72 | $filter = new Filter(); 73 | $filter->add('testVal', function($value){ 74 | return 'returned: '.$value; 75 | }); 76 | 77 | $input = new Input($filter); 78 | $input->set('get','testVal',$validEmail); 79 | 80 | $result = $input->get('testVal'); 81 | 82 | $this->assertEquals('returned: '.$validEmail,$result); 83 | } 84 | } -------------------------------------------------------------------------------- /Shield/tests/Shield/SessionTest.php: -------------------------------------------------------------------------------- 1 | _di = new Di(); 28 | $this->_filter = new Filter($this->_config); 29 | $this->_input = new Input($this->_filter); 30 | //$this->_config = new Config(); 31 | 32 | // $this->_di->register($this->_filter); 33 | 34 | // $this->_di->register(array( 35 | // $this->_input, $this->_config 36 | // )); 37 | 38 | $this->_session = new Session(); 39 | 40 | //$this->_di->register($this->_session); 41 | 42 | session_start(); 43 | } 44 | public function tearDown() 45 | { 46 | $this->_di = null; 47 | } 48 | 49 | public function testSetSessionValue() 50 | { 51 | global $_GET; 52 | global $_POST; 53 | global $_REQUEST; 54 | global $_SERVER; 55 | global $_FILES; 56 | global $_SESSION; 57 | 58 | $testVal = '12345'; 59 | 60 | $this->_input->set('session','testval',$testVal); 61 | $result = $this->_input->session('testval'); 62 | 63 | $this->assertEquals($testVal,$result); 64 | } 65 | } -------------------------------------------------------------------------------- /Shield/tests/Shield/ShieldTest.php: -------------------------------------------------------------------------------- 1 | _shield = null; 19 | } 20 | 21 | /** 22 | * Test that an exact match route is correctly handled 23 | * 24 | * @return null 25 | */ 26 | public function testRouteMatch() 27 | { 28 | $_SERVER['REQUEST_URI'] = '/'; 29 | $_SERVER['REQUEST_METHOD'] = 'GET'; 30 | 31 | $app = new Shield(); 32 | $app->get('/', function(){ 33 | echo 'match /'; 34 | }); 35 | 36 | ob_start(); 37 | $app->run(); 38 | 39 | $output = ob_get_clean(); 40 | $this->assertEquals('match /', $output); 41 | } 42 | 43 | /** 44 | * Test that a regex route is matched correctly 45 | * 46 | * @return null 47 | */ 48 | public function testRegexRouteMatch() 49 | { 50 | $_SERVER['REQUEST_URI'] = '/testing123'; 51 | $_SERVER['REQUEST_METHOD'] = 'GET'; 52 | 53 | $app = new Shield(); 54 | $app->get('/testing[0-9]+', function(){ 55 | echo 'match /'; 56 | }); 57 | 58 | ob_start(); 59 | $app->run(); 60 | 61 | $output = ob_get_clean(); 62 | $this->assertEquals('match /', $output); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Shield/tests/Shield/ViewTest.php: -------------------------------------------------------------------------------- 1 | _di = new Di(); 15 | Config::load(); 16 | $this->_filter = new Filter($this->_config); 17 | $this->_template = new View($this->_filter); 18 | 19 | //$this->_di->register(new View($this->_template)); 20 | } 21 | public function tearDown() 22 | { 23 | $this->_di = null; 24 | $this->_template = null; 25 | $this->_config = null; 26 | } 27 | 28 | /** 29 | * Test that a template with a replacement is correctly rendered 30 | * 31 | * @return null 32 | */ 33 | public function testTemplateReplace() 34 | { 35 | $templateString = 'testing [this]'; 36 | $this->_template->this = 'foo'; 37 | $this->assertEquals( 38 | $this->_template->render($templateString), 39 | 'testing foo' 40 | ); 41 | } 42 | 43 | /** 44 | * Test that a value can be correctly on the template object 45 | * 46 | * @return null 47 | */ 48 | public function testGetSetValue() 49 | { 50 | $this->_template->testing = 'foo'; 51 | $this->assertEquals($this->_template->testing,'foo'); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Shield/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine on 3 | 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | 7 | RewriteRule ^(.*)$ index.php/$1 [L] 8 | -------------------------------------------------------------------------------- /app/config.php: -------------------------------------------------------------------------------- 1 | '/tmp' 8 | 9 | /** 10 | * Put in the IP addresses of allowed hosts here 11 | */ 12 | //'allowed_hosts' => array('127.0.0.1') 13 | 14 | /** 15 | * Session management settings 16 | */ 17 | 'session' => array( 18 | /** 19 | * Turn on/off session locking. Locking binds the session to the user's 20 | * IP address and User-Agent combo to help prevent session fixation & guessing 21 | */ 22 | 'lock' => false, 23 | 24 | /** 25 | * Use this to set the sessions directory 26 | */ 27 | 'path' => '/tmp', 28 | 29 | /** 30 | * Use this to set the cipher key for the session to your value 31 | */ 32 | 'key' => null 33 | ), 34 | 35 | /** 36 | * Force the site to run under HTTPS (will try to redirect if HTTP) 37 | */ 38 | 'force_https' => false, 39 | 40 | /** 41 | * View options 42 | */ 43 | 'view' => array( 44 | 'charset' => 'utf-8', 45 | 'content-type' => 'text/html' 46 | ) 47 | ); 48 | -------------------------------------------------------------------------------- /app/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shield : A Security-Minded Microfrmaework 5 | 9 | 10 | 11 |
12 |

Shield Framework Examples

13 | 24 | 25 |
26 | get('/', function() use ($app){ 35 | 36 | $app->view->set(array( 37 | 'fake' => 'link text', 38 | 'test' => 'foodles' 39 | )); 40 | return $app->view->render(' 41 |

Basic Routing & Escaping

42 |

43 | This route sets two values to the view "fake" & "test". The "fake" 44 | value has HTML in it and is, by default, filtered. 45 |

46 |
Output
47 | index1: [fake] 48 | '); 49 | }); 50 | 51 | /** 52 | * Setting up filters on data (desides the default) 53 | */ 54 | $app->get('/filters', function() use ($app){ 55 | 56 | $app->view->set(array( 57 | 'test' => $app->input->get('test'), 58 | 'test1' => $app->input->get('test1') 59 | )); 60 | 61 | $app->filter->add(array( 62 | 'test' => 'email', 63 | 'test1' => function($value){ 64 | return 'custom filter: '.$value; 65 | } 66 | )); 67 | 68 | return $app->view->render(' 69 |

Built-in & Custom Filters

70 |

71 | There are several built-in filtering types including "email", "integer" and "url" 72 | to help you sanitize your data.
73 | You can also add custom filters using PHP\'s closures. 74 |

75 |
Output
76 | TEST1: [test1]
77 | TEST: [test] 78 | '); 79 | }); 80 | 81 | /** 82 | * Shows how to use a custom configuration with a route 83 | * NOTE: This page is in an HTML template, so this XML won't render correctly 84 | * (but you get the idea....) 85 | */ 86 | $app->get('/cfg', function() use ($app){ 87 | 88 | $app->view->set('output','my output'); 89 | 90 | return $app->view->render(' 91 | 92 | 93 | [output] 94 | 95 | '); 96 | 97 | }, array( 98 | 'view.content-type' => 'text/xml' 99 | )); 100 | 101 | /** Execute the application */ 102 | $app->run(); 103 | ?> 104 |
105 | 106 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"shield/shield", 3 | "type":"library", 4 | "description":"Shield : Microframework, Major Security", 5 | "keywords":["microframework", "rest", "router"], 6 | "homepage":"https://github.com/enygma/shieldframework.git", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"Chris Cornutt", 11 | "email":"ccornutt@phpdeveloper.org", 12 | "homepage":"http://www.phpdeveloper.org/" 13 | } 14 | ], 15 | "require":{ 16 | "php":">=5.3.1" 17 | }, 18 | "autoload":{ 19 | "psr-0":{ 20 | "Shield":"Shield/" 21 | } 22 | } 23 | } --------------------------------------------------------------------------------