diff --git a/.project b/.project new file mode 100644 index 0000000..85d8aed --- /dev/null +++ b/.project @@ -0,0 +1,12 @@ + + + BOTK-context + + + + + + + com.aptana.editor.php.phpNature + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a1dbfdb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +Change logs +=========== +0.0.1-alpha +----------- + * code, doc and tests completed \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d70c86e --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +botk/context package +==================== +This is a BOTK package. Please refer to http://ontology.it/tools/botk for more info +about BOTK project. + +1. Package documentation is in doc directory. +2. Unit tests in tests directory. + cd tests; phpunit --coverage-html ./report +3. Some code examples in samples directory. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5873b43 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "botk/context", + "description": "ini file management, sanitization, validation", + "type": "library", + "license": "GPL-3.0+", + "homepage": "http://ontology.it/tools/botk4/context/", + "keywords": ["E-Artspace", "BOTK", "Sanitization", "Validation"], + "autoload": { + "psr-0": { + "BOTK\\Context": "library/" + } + }, + "authors": [ + { + "name": "Enrico fagnoni", + "email": "e.fagnoni@e-artspace.com" + } + ], + "require": { + "php": ">=5.3" + } +} \ No newline at end of file diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..bbcd926 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,296 @@ + + + + + Context package documentation + + + +
+
+

Context package

+

Ultraligth validation and sanitization for RESTful APIs.

+
+
+
+
Author:
+
+
Enrico Fagnoni - E-Artspace
+
+
+
+
+

Abstract

+

This package exports a set of class to manage the context of a RESTful request.

+

A Context is defined as the full set of variables available runtime durin the serving of an http request. + Variables are grouped in distinct name spaces.

+

All variables in name spaces are read only.

+

This package exposes the methods to sanitize and validate context variables.

+

Note that cookies and session aren't part of context. This because proper RESTful resource implementation + shouldn't need persisten status in any way.

+
+
+

License

+
Copyright © 2013 by E-Artspace + S.r.L.® GPL-3.0+ +

This code is free software; you can redistribute it and/or modify it under the terms of the GNU + General Public License as published by the Free Software Foundation; either version 3.0of the License, + or (at your option) any later version. This code is distributed in the hope that it will be useful,but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details.

+

For commercial license or others license schema, please contact E-Artspace + or authors.

+
+
+ +
+

Installation

+

This package follows BOTK guide line for installation and require composer.

+

Add following dependances to composer.json file in your project root:

+
{
+    "require": {
+        "botk/context": "*"
+    }
+}
+

+
+
+

The Context class

+

A Context is the full set of all that a CGI endpoint knows about  Request beside its URI. Context space + contains system environment variables, requests header, request body, server specific vars, cookies, variables + in configuration files, local define variables, etc etc..
+

+

Context variables are grouped in namespaces. BOTK\Context\Context class provide the + method  ns(namespace name) to access name spaces. There are five predefined + namespaces:

+ +
    +
  1. the variables defined in the heading of the request and exposed by web server (INPUT_SERVER);
  2. +
  3. the variables encoded in http URI (INPUT_GET);
  4. +
  5. the variables encoded in http body (INPUT_POST);
  6. +
  7. the system environment (INPUT_ENV);
  8. +
  9. local defined variables (Context::LOCAL).
  10. +
+ +

Beside these, Context class allow you to define dynamic namespace built from .ini files loaded runtime from a + configuration directory. The name of the namespace is the name of the configuration file without .ini extension.

+

The configuration file must exists when is first referenced. The default configuration directory is + ../configs, that normally is a directory placed at the same level of the directory that contains the scripts who + instance a context class. You can change the default directory defining the environment variable $_ENV['BOTK_CONFIGDIR']. +

+

Context shold be considered as a readonly data structure because you cant change the resource execution + context.

+

For example suppose to have a end-point script is in myapp/httdocs/index.php so, to get a + success on following code, it must exist myapp/configs/sample.ini file:

+
use BOTK\Context\Context as CX;
+$sampleNameSpace = CX::factory()->ns('sample');
+
+

If you want to put the .ini file in the same end-point directory:

+
use BOTK\Context\Context as CX;
+$_ENV['BOTK_CONFIGDIR'] = '.';
+$sampleNameSpace = CX::factory()->ns('sample');
+
+

You can also access to local variable passing them to context constructor:

+
$myvar = 'ok'; // define a variable in local scope
+$v = CX::factory(get_defined_vars())->ns(CX::LOCAL)->getValue('myvar');
+//$v == 'ok'
+
+

Context class exposes the method guessRequestCanonicalUri() that returns the current request uri + in canonical form and the method guessRequestRelativelUri() that returns it as relative path.

+

Please note that get request uri is not a trivial job, this because http servers and proxyes can change what + the user entered. You can just guess about it.

+

The ContextNameSpace Class

+

This class implements the controlled access to set of variables.

+
+
+

The getValue() method

+

The BOTK\Context\ContextNameSpace class exports the method getValue() that + allows to validate and sanitize variables defined in a namespace (see PHP + Validation & Sanitization article).

+

If somethings goes wrong it throwns an error that can be trapped by standard BOTK error management.

+

getValue() exposes following interface:

+
  param string $varName       the name of a variable defined in the namespace
+  param mixed  $default       a default value if variable is not set. NULL means that
+                              the variable is mandatory.
+  param mixed  $validator     Can be:
+                                1) null use default validation
+                                2) an integer representing a simple Validate filter without flags and options
+                                3) an array representing a Validate filter with flags and/or options
+                                4) an instance of an object that expose function 'assert'.
+                                   you can use istance of Respect\Validation\Validator
+  param mixed  $sanitizer     Can be:
+                                A) null if no sanitization is required
+                                B) an integer representing a simple Sanitize filter without flags and options
+                                C) an array representing a Sanitize filter with flags and/or options
+                                D) a callable that accept an argument (the source) and return sanitized one 
+ 
+ throws InvalidArgumentException if $varName is not defined and $default not provided
+ throws Exception if validator fails to validate variable (depending from the validator)
+ throws InvalidArgumentException if sanitize fails
+ 
+ return mixed the sanitized value associated to varName in namespace
+ 
+ LIMITS:
+  sanitizer are supported just on scalar values
+  non scalar value require a custom validator like Respect\Validator
+ 
+
+

In addition to getValue() ContextNameSpace class expose a set of shortcuts for + easy to read and write code.

+

validator shortcucts

+

Some validators shold be boring to write, here are a set of predefined shortcuts:

+
     public static function ENUM($wlist)
+     {
+         return  array(
+                'filter'    => FILTER_VALIDATE_REGEXP,
+                'options'   => array('regexp' => "/^($wlist)$/")
+         );
+     }
+     
+     public static function STRING($pattern)
+     {
+         return  array(
+                'filter'    => FILTER_VALIDATE_REGEXP,
+                'options'   => array('regexp' => $pattern)
+         );
+     }
+
+     public static function FILENAME()
+     {
+         return  array(
+                'filter'    => FILTER_VALIDATE_REGEXP,
+                'options'   => array('regexp' => '^[\w,\s-]+\.[A-Za-z]+$')
+         );
+     }  
+
+
+     public static function POSITIVE_INT()
+     {
+         return  array(
+                'filter'    => FILTER_VALIDATE_INT,
+                'options'   => array("min_range"=>1)
+         );
+     }
+
+
+     public static function NON_NEGATIVE_INT()
+     {
+         return  array(
+                'filter'    => FILTER_VALIDATE_INT,
+                'options'   => array("min_range"=>0)
+         );
+     }  
+


+

+

getValue shortcucts

+

Even with validator shortcuts, a call to getValue method can be too verbose, here are some shortcut you can use + for common task:

+ +


+

+

PagedResourceContext class

+

The PagedResource class is a facility to manage the context of a resource whose query string can contains + variables to drive pagination processing. It is a specialization of Context class and like Context should be + considered as a read-only data structure.

+ The resource paging context is inspired on W3C's Linked Data Platform Paging specifications .Here are some + imported definitions:
+
+
Paged resource
+
A resource  whose representation may be too large to fit in a single HTTP response, for which a server + offers a sequence of single-page resources. A paged P is broken into a sequence of pages + (single-page resources) P1, P2, ...,Pn, the representation of + each Pi contains a subset of  P..
+
Single-page resource
+
One of a sequence of related resources P1, P2, ...,Pn, each of + which contains a subset of the state of another resource P. P is called the paged + resource. For readers familiar with paged feeds, a single-page resource is similar to a feed document and the + same coherency/completeness considerations apply. +

Note: the choice of terms was designed to help authors and readers clearly differentiate between the resource + being paged, and the individual page resources, + in cases where both are mentioned in close proximity.

+
+
first page
+
+
The uri to the first single-page + resource of a paged resource P. For example + http://www.example.org/bigresource?page=0
+
next page
+
+
The uri  to the next single-page resource of a paged + resource P.
+
last page
+
+
The uri  to the last single-page resource of a paged + resource P.
+
previous page
+
+
The uri  to the previous single-page resource of a paged + resource P.
+
+

Pagination require some variable to be specified in resource URI query strings. Such variables name and other + default values can be passed to class constructor as an option associative array. Here are supported keywords:

+ +

It exposes following methods:

+
+
getSinglePageResourceUri( $pagenum = null, $pagesize=null)
+
returns the canonical uri with page variables in query string. Without arguments returns current page + resource uri
+
getPagedResourceUri()
+
return the resource uri wit page variables removed from query string 
+
isPagedResource()
+
returns true is resource uri contains page info
+
getPageNum()
+
returns the current page num
+
getPageSize()
+
returns the size of the page
+
firstPageUri()
+
returns the Resource uri for first Single page resource
+
nextPageUri()
+
returns the Resource uri for next page resource or the current page resource uri
+
prevPageUri()
+
returns the Resource uri for next page resource or the first page resource uri
+
hasNextPage()
+
returns true if a next page is available. By default returns true. It can be affected by declareActualPageSize() + method
+
isLastPage()
+
returns true if the current is the last page. By default returns false. It can be affected by declareActualPageSize() + method
+
declareActualPageSize($count)
+
inform context of the number of item in current Page, by default is 0. This affects hasNextPage() + and isLastPageMethods().
+
+

Here is how declaretActualPageSize() affects hasNextPage() and isLastPage():

+
    function hasNextPage(){
+         return actualPageSize == $this->pagesize;
+    }
+      
+    function isLastPage(){
+          return actual PageSize < $this->pagesize;
+    }
+
+
+


+

+

+
+ + + diff --git a/library/BOTK/Context/Context.php b/library/BOTK/Context/Context.php new file mode 100644 index 0000000..fb1b1fa --- /dev/null +++ b/library/BOTK/Context/Context.php @@ -0,0 +1,166 @@ +nameSpaces = array( + INPUT_POST => new ContextNameSpace($_POST), + INPUT_GET => new ContextNameSpace($_GET), + INPUT_ENV => new ContextNameSpace($_ENV), + INPUT_SERVER => new ContextNameSpace($_SERVER), + self::LOCAL => new ContextNameSpace($localVars) + // This is supposed a RESTfull toolkit ... no COOKIE, no SESSION no GLOBALS by design :-) + ); + + // initialize external namespace technology + $this->configureIniStore(); // this is for namespace maintained in ini file + } + + + /* + * Where to find config files + * Fall back priorities + * case 1) use the one provided as paramether + * case 2) check the one provided in BOTK_CONFIGDIR environment variable + * case 3) use ../configs relative to application script file + */ + public function configureIniStore( $configDir=null) + { + $this->configDir = $configDir // case 1) + ? $configDir + : ( isset($_ENV['BOTK_CONFIGDIR']) // case 2 + ? $_ENV['BOTK_CONFIGDIR'] + : (dirname(dirname($_SERVER['SCRIPT_FILENAME'])) . '/configs') + ); // case 3 + + return $this; + } + + + private function loadArrayFromIniFile($name) + { + static $inifilesDB = array(); // a database of variables loaded from .ini files + + if (!isset($inifilesDB[$name])) { + // Let error management take care of errors... + $iniFile = "$this->configDir/$name.ini"; + $inifilesDB[$name]= file_exists($iniFile) + ?parse_ini_file($iniFile, true) + :array(); + if (!is_array($inifilesDB[$name])){ + throw new \InvalidArgumentException("Invalid configuration file in $iniFile.",404); + } + } + + return $inifilesDB[$name]; + } + + /* + * $nameSpace can be: + * . a predefined name space idenfifier (INPUT_POST, INPUT_GET, INPUT_ENV,INPUT_SERVER ) + * . a string containing a file name that mst exists in configDir with .ini extension + * + */ + public function ns($nameSpace) + { + // If ! exist try to load a configuration file + if ( !isset($this->nameSpaces[$nameSpace]) ) { + // short sanitize of nameSpace + if (!filter_var( $nameSpace,FILTER_VALIDATE_REGEXP, + array('options'=>array('regexp' => '/^[\w]+$/')))) { + throw new InvalidArgumentException("Invalid configuration name.",400); + } + $this->nameSpaces[$nameSpace] = new ContextNameSpace($this->loadArrayFromIniFile($nameSpace)); + } + + return $this->nameSpaces[$nameSpace]; + } + + /* + * This helper tryes to return the canonical uri of current http requests. + * It is inspired by Joomla JURI:base() implementation + * + * Tested with apache server. + */ + public function guessRequestCanonicalUri() + { + $server = $this->ns(INPUT_SERVER); + + // Determine if the request was over SSL (HTTPS). + if( (strtolower($server->getValue('HTTPS','off')) != 'off')) { + $schema = 'https://'; + $defaultPort=443; + } else { + $schema = 'http://'; + $defaultPort=80; + } + + $port = ($server->getValue('SERVER_PORT',$defaultPort, ContextNameSpace::NON_NEGATIVE_INT())); + $http_host = $server->getValue('HTTP_HOST','localhost'); + $authority = ($port===$defaultPort) + ?($http_host) + :($http_host . ':'. $port); + + return $schema.$authority.$this->guessRequestRelativeUri(); + } + + + /* + * This helper tryes to return the relative uri of current http requests. + * We need this since in IIS we do not have REQUEST_URI to work with, we will assume we are + * and so will therefore need to work some magic with the SCRIPT_NAME + * + * Tested with apache server. + */ + public function guessRequestRelativeUri() + { + $server = $this->ns(INPUT_SERVER); + + $php_self = $server->getValue('PHP_SELF',''); + $request_uri = $server->getValue('REQUEST_URI',''); + + if (!empty($php_self) && !empty($request_uri)) + { + $theURI = $request_uri; + } + else + { + // Since we do not have REQUEST_URI to work with, we will assume we are + // running on IIS and will therefore need to work some magic with the SCRIPT_NAME and + // QUERY_STRING environment variables. + $script_name = $server->getValue('SCRIPT_NAME',''); + $query_string = $server->getValue('QUERY_STRING',''); + + + // IIS uses the SCRIPT_NAME variable instead of a REQUEST_URI variable... thanks, MS + $theURI = $script_name; + + // If the query string exists append it to the URI string + if (!empty($query_string)) + { + $theURI .= '?' . $query_string; + } + } + + return $theURI; + } +} diff --git a/library/BOTK/Context/ContextNameSpace.php b/library/BOTK/Context/ContextNameSpace.php new file mode 100644 index 0000000..b946db1 --- /dev/null +++ b/library/BOTK/Context/ContextNameSpace.php @@ -0,0 +1,236 @@ +ns(INPUT_SERVER); + * $ip = $serverNameSpace->getValue('SERVER_ADDR', Context::MANDATORY, FILTER_VALIDATE_IP); + * $port = $serverNameSpace->getValue('SERVER_PORT', '8080', array ( + * 'filter' => FILTER_VALIDATE_INT, + * 'flags' => FILTER_REQUIRE_SCALAR, + * 'options' => array('min_range' => 8080, 'max_range' => 8084) + * ) + * + * You can also use Respect\Validation library for complex validator: + * $port = $serverNameSpace->getValue('SERVER_PORT', '8080',v::int()->between(8080,8084)) + * + * You can validate and sanitize in one call: + * + * $port = $serverNameSpace->getValue('SERVER_PORT', '8080', v::int()->between(8080,8084),FILTER_SANITIZE_NUMBER_INT); + * + * You can use Context helper hortcuts: + * $method = $serverNameSpace->getValue('REQUEST_METHOD', 'GET', $c->ENUM('GET|POST')) + * + * Note that reurned value can be a scalar or an array. + */ +class ContextNameSpace +{ + const MANDATORY = null; // just syntactic sugar for null value + + protected $varStore = array(); + + /** + * + * @param $theArray can be an array or a reference to a predefinite array (es. $_POST) + */ + public function __construct( $theArray ) + { + $this->varStore = $theArray; + } + + + /** + * Sanitize and validate a variable contained in a name space + * + * @param string $varName the name of a variable defined in the namespace + * @param mixed $default a default value if variable is not set. NULL means that + * the variable is mandatory. + * @param mixed $validator Can be: + * 1) null use default validation + * 2) an integer representing a simple Validate filter without flags and options + * 3) an array representing a Validate filter with flags and/or options + * 4) an instance of an object that expose function 'assert'. + * you can use istance of Respect\Validation\Validator + + * @param mixed $sanitizer Can be: + * A) null if no sanitization is required + * B) an integer representing a simple Sanitize filter without flags and options + * C) an array representing a Sanitize filter with flags and/or options + * D) a callable that accept an argument (the source) and return sanitized one + * + * @throws InvalidArgumentException if $varName is not defined and $default not provided + * @throws Exception if validator fails to validate variable (depending from the validator) + * @throws InvalidArgumentException if sanitize fails + * + * @return mixed the sanitized value associated to varName in namespace + * + * LIMITS: + * sanitizer are supported just on scalar values + * non scalar value require a custom validator like Respect\Validator + * + */ + public function getValue ( + $varName, + $default = null, + $validator=null, + $sanitizer=null + ) + { + // prepare a static array to validate validators and sanitizer filters + static $validFilters = null; //load once runtime + if (is_null($validFilters)){ + // prepare a list of valid filters: do once + $validFilters= array(); + $filters = filter_list(); + foreach($filters as $filter_name) { + $validFilters[filter_id($filter_name)] = $filter_name; + } + // Remove unsupported filters: + foreach( array(FILTER_VALIDATE_BOOLEAN,FILTER_CALLBACK ) as $badFilter){ + unset($validFilters[$badFilter]); + } + } + + // Ensure var exists or a default value is present + if (!$varName + || (is_null($default) && !isset($this->varStore[$varName]))) { + throw new InvalidArgumentException("Missing mandatory parameter $varName.",400); + } + + $sourceValue = isset($this->varStore[$varName])?$this->varStore[$varName]:$default; + $scalarValue = is_scalar($sourceValue); + + + // sanitize the variable value (if requested ) + if (is_null($sanitizer)) { + // CASE A: sanitizing not required + $sanitizedValue = $sourceValue; + } elseif (is_int($sanitizer) && array_key_exists($sanitizer,$validFilters) && $scalarValue) { + // CASE B: Use a simple sanitize filter + $sanitizedValue = filter_var($sourceValue, $sanitizer); + } elseif (is_array($sanitizer) + && isset($sanitizer['filter']) + && array_key_exists($sanitizer['filter'],$validFilters) + && $scalarValue + ) { + // CASE C: use sanitizer filter with flags and/or options + $filter = $sanitizer['filter']; + $sanitizedValue = filter_var($sourceValue, $filter, $sanitize); // should I unset($sanitizer['filter']) ? + } elseif (is_callable($sanitizer)) { + $sanitizedValue = $sanitizer($sourceValue); + } else { + throw new InvalidArgumentException("Invalid sanitizer filter on $varName.",400); + } + + assert(isset($sanitizedValue)); + //Now we are sure that variable has a sanitized value: validate it (if requested ) + + if (is_null($validator)) { + // CASE 1: validation not required + $valid = $scalarValue?filter_var($sanitizedValue):$sanitizedValue; + $validatorName='DEFAULT'; + } elseif (is_int($validator) + && array_key_exists($validator,$validFilters) + && $scalarValue ) { + // CASE 2: Use a simple validation filter + $valid = filter_var($sanitizedValue, $validator); + $validatorName=$validFilters[$validator]; + } elseif (is_array($validator) + && isset($validator['filter']) + && array_key_exists($validator['filter'],$validFilters) + && $scalarValue ){ + // CASE 3: use validation filter with flags and/or options + $filter = $validator['filter']; + $valid = filter_var($sanitizedValue, $filter, $validator); // should unset($validator['filter'])? + $validatorName=$validFilters[$filter]; + } elseif (is_object($validator)) { + // CASE 4: the object must expose assert() method (like in Respect\Validation library ) + try { + $validator->assert($sanitizedValue); // N.B. is up to custom validator test the value type + $valid=true; + } catch ( Exception $e){ + $validatorName='OBJECT_VALIDATOR'; + $valid = false; + $errorMsg=$e->getMessage(); + } + } else { + $validatorName='unsupported'; + $valid=false; + } + + // thrown exception if not validate value + if (false===$valid) { + if (empty($errorMsg)) { + $val=$scalarValue?$sanitizedValue:'structured'; + $errorMsg = "Invalid value($val) for $validatorName filter on [$varName]"; + } + throw new InvalidArgumentException($errorMsg,400); + } + + return $sanitizedValue; + } + + /* + * Here are some helpers for shortcuts + */ + public function getString($varName,$default = null) + { + return $this->getValue($varName,$default, null, FILTER_SANITIZE_STRING); + } + + public function getURI($varName,$default = null) + { + return $this->getValue($varName,$default, self::STRING('/.+/'), FILTER_SANITIZE_URL); + } + + + + /* + * Here are some helpers for validator shortcuts + */ + public static function ENUM($wlist) + { + return array( + 'filter' => FILTER_VALIDATE_REGEXP, + 'options' => array('regexp' => "/^($wlist)$/") + ); + } + + public static function STRING($pattern) + { + return array( + 'filter' => FILTER_VALIDATE_REGEXP, + 'options' => array('regexp' => $pattern) + ); + } + + public static function FILENAME() + { + return array( + 'filter' => FILTER_VALIDATE_REGEXP, + 'options' => array('regexp' => '/^[\w,\s-]+\.[A-Za-z]+$/') + ); + } + + + public static function POSITIVE_INT() + { + return array( + 'filter' => FILTER_VALIDATE_INT, + 'options' => array("min_range"=>1) + ); + } + + + public static function NON_NEGATIVE_INT() + { + return array( + 'filter' => FILTER_VALIDATE_INT, + 'options' => array("min_range"=>0) + ); + } +} diff --git a/library/BOTK/Context/PagedResourceContext.php b/library/BOTK/Context/PagedResourceContext.php new file mode 100644 index 0000000..ad62013 --- /dev/null +++ b/library/BOTK/Context/PagedResourceContext.php @@ -0,0 +1,94 @@ + 'page', + 'pslabel' => 'pagesize', + 'pagesize' => 100 + ); + protected $pagesize,$pagenum,$pagedRequested; + + public function __construct( $options = null) + { + if (!is_array($options)) $options = array(); + parent::__construct(array_merge($this->defaults,$options)); + + $queryString= $this->ns(INPUT_GET); + $options= $this->ns(self::LOCAL); + + $ps = $queryString->getValue($options->getValue('pslabel'), 0, FILTER_VALIDATE_INT); + $p = $queryString->getValue($options->getValue('plabel'), -1, FILTER_VALIDATE_INT); + + $this->pagedRequested = ($ps>0 || $p>=0); + $this->pagenum = max(0,$p); + $this->pagesize= ($ps<1)?($options->getValue('pagesize')):$ps; + } + + + /* + * Returns resource canonical uri with page info data in quesry string. + * You can override request page info with passed paramethers + */ + public function getSinglePageResourceUri( $pagenum = null, $pagesize=null) + { + if( is_null($pagenum) ) $pagenum =$this->pagenum; + if( is_null($pagesize)) $pagesize =$this->pagesize; + + //rebuild query string from already parsed _GET superglobal but do not override it. + $vars = $_GET; + $o=$this->ns(self::LOCAL); + $vars[$o->getValue('plabel')] = $pagenum; + $vars[$o->getValue('pslabel')] = $pagesize; + + return $this->getQueryStrippedUri() . '?'. http_build_query($vars); + } + + + public function getPagedResourceUri() + { + static $cachedUri = null; + if (is_null($cachedUri)){ + $uri = $this->getQueryStrippedUri(); + + //rebuild query string from already parsed _GET superglobal but do not override it. + $vars = $_GET; + $o=$this->ns(self::LOCAL); + unset($vars[$o->getValue('plabel')]); + unset($vars[$o->getValue('pslabel')]); + $cachedUri = count($vars) + ?($uri.'?'. http_build_query($vars)) + :$uri; + } + + return $cachedUri; + } + + + public function getQueryStrippedUri() + { + static $cachedUri = null; + if (is_null($cachedUri)){ + $cachedUri = $this->guessRequestCanonicalUri(); + //strip query string + $cachedUri = preg_replace('/\?.*/','',$cachedUri); + } + + return $cachedUri; + } + + public function isSinglePageResource(){ return $this->pagedRequested;} + public function getPageNum(){ return $this->pagenum;} + public function getPageSize(){ return $this->pagesize;} + + /* + * Some helpers + */ + public function firstPageUri(){ return $this->getSinglePageResourceUri(0);} + public function nextPageUri(){return $this->getSinglePageResourceUri($this->pagenum+1);} + public function prevPageUri(){ return $this->getSinglePageResourceUri(max(0,$this->pagenum-1));} +} diff --git a/samples/getsample.php b/samples/getsample.php new file mode 100644 index 0000000..845f567 --- /dev/null +++ b/samples/getsample.php @@ -0,0 +1,82 @@ +Get $param with $prompt:
"; + try { + print_r($cx->ns($ns)->getValue($param,$default,$validator,$sanitizer)); + } catch ( Exception $e ){ + echo $e->getMessage(); + } + echo '
'; +} +?> + + + Test sanitizing and validate context variables + + + + + +

Try call this script with ....

+ +
+

script result:

+
+FILTER_VALIDATE_INT, + 'options'=> array('min_range' => 0, 'max_range' => 2) + ),FILTER_SANITIZE_NUMBER_INT); + +// Optional par2 +test(INPUT_GET, 'Default', 'par2', 1); +test(INPUT_GET, 'ENUM (1|0)', 'par2', 1, V::ENUM('1|0')); +test(INPUT_GET, 'FILTER_VALIDATE_INT', 'par2', 1, FILTER_VALIDATE_INT,FILTER_SANITIZE_NUMBER_INT); +test(INPUT_GET, 'FILTER_VALIDATE_INT (0-2)', 'par2', 1, array( + 'filter'=>FILTER_VALIDATE_INT, + 'options'=> array('min_range' => 0, 'max_range' => 2) + ),FILTER_SANITIZE_NUMBER_INT); + +// Mandatory var1 +test('sample', 'default filter', 'var1'); +test('sample', 'ENUM (yes|no)', 'var1', V::MANDATORY, V::ENUM('yes|no')); +test('sample', 'FILTER_VALIDATE_INT', 'var1', V::MANDATORY, FILTER_VALIDATE_INT,FILTER_SANITIZE_NUMBER_INT); +test('sample', 'FILTER_VALIDATE_INT (0-2)', 'var1', V::MANDATORY, array( + 'filter'=>FILTER_VALIDATE_INT, + 'options'=> array('min_range' => 0, 'max_range' => 2) + ),FILTER_SANITIZE_NUMBER_INT); + +// Optional var2 +test('sample', 'Default', 'var2', 1); +test('sample', 'ENUM (1|0)', 'var2', 1, V::ENUM('1|0')); +test('sample', 'FILTER_VALIDATE_INT', 'var2', 1, FILTER_VALIDATE_INT); +test('sample', 'FILTER_VALIDATE_INT (0-2)', 'var2', 1, array( + 'filter'=>FILTER_VALIDATE_INT, + 'options'=> array('min_range' => 0, 'max_range' => 2) + ),FILTER_SANITIZE_NUMBER_INT); + +?> +
+
+ + + \ No newline at end of file diff --git a/samples/sample.ini b/samples/sample.ini new file mode 100644 index 0000000..80770e1 --- /dev/null +++ b/samples/sample.ini @@ -0,0 +1,13 @@ +; This is a simple test ini file +var1 ="this is a string" +var2 = 2 +var3[] = "array-1" +var3[] = "array-2" + + +; +; Sections ignored +; +[Section] +var4 = 4 +var5= false diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..70460fd --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,4 @@ +ns('sample')->getValue('novar','ok'); + $this->assertEquals($v,'ok'); + } + + + public function testVarDefaultMandatoryExists() { + $v = CX::factory()->ns('sample')->getValue('var1'); + $this->assertEquals($v,"This is a string"); + } + + + + public function testVarArray() { + $v = CX::factory()->ns('sample')->getValue('var3'); + $this->assertEquals($v,array(1,2,3)); + } + + + /** + * @expectedException \Exception + * + */ + public function testVarDefaultMandatoryNo() { + $v = CX::factory()->ns('sample')->getValue('novar'); + } + + + + /** + * @expectedException \Exception + * + */ + public function testNoNamespace() { + $v = CX::factory()->ns('nosample')->getValue('novar',1); + } +} diff --git a/tests/library/BOTK/Context/Context/LocalContextTest.php b/tests/library/BOTK/Context/Context/LocalContextTest.php new file mode 100644 index 0000000..ddd609c --- /dev/null +++ b/tests/library/BOTK/Context/Context/LocalContextTest.php @@ -0,0 +1,22 @@ +ns(CX::LOCAL)->getValue('myvar'); + $this->assertEquals($v,'ok'); + } + +} diff --git a/tests/library/BOTK/Context/ContextNameSpace/SanitizeTest.php b/tests/library/BOTK/Context/ContextNameSpace/SanitizeTest.php new file mode 100644 index 0000000..96e20cd --- /dev/null +++ b/tests/library/BOTK/Context/ContextNameSpace/SanitizeTest.php @@ -0,0 +1,56 @@ +ns = new ContextNameSpace( array( + 'p1' => '-string1', + 'p2' => 10, + 'p3' => array(1,2,3), // same type array + 'p4' => array(1,2,'ff'), // mixed type array ( unsupported by std filter, need custom validator) + 'p5' => new \stdClass, // unsupported both by ini file and ContextNameSpace, but... + )); + } + + public function testSimpleSanitize() + { + $p = $this->ns->getValue('p1',null,null, FILTER_SANITIZE_NUMBER_INT); + $this->assertEquals(-1,$p); + } + + + public function testSanitizeAndValidate() + { + $p = $this->ns->getValue('p1', null ,FILTER_VALIDATE_INT, FILTER_SANITIZE_NUMBER_INT); + $this->assertEquals(-1,$p); + } + + + public function testSanitizeAndValidateOnDefault() + { + $p = $this->ns->getValue('unesistent', 'thisis2' ,FILTER_VALIDATE_INT, FILTER_SANITIZE_NUMBER_INT); + $this->assertEquals(2,$p); + } + + + + public function testSanitizeWithCallable() + { + $p = $this->ns->getValue('unesistent', 'plain' , null, 'ucfirst'); + $this->assertEquals('Plain',$p); + } +} diff --git a/tests/library/BOTK/Context/ContextNameSpace/ValidationFiltersTest.php b/tests/library/BOTK/Context/ContextNameSpace/ValidationFiltersTest.php new file mode 100644 index 0000000..7787be6 --- /dev/null +++ b/tests/library/BOTK/Context/ContextNameSpace/ValidationFiltersTest.php @@ -0,0 +1,163 @@ +ns = new ContextNameSpace( array( + 'p1' => 'stringa', + 'p2' => 10, + 'p3' => array(1,2,3), // same type array + 'p4' => array(1,2,'ff'), // mixed type array ( unsupported by std filter, need custom validator) + 'p5' => new \stdClass, // unsupported both by ini file and ContextNameSpace, but... + )); + } + + public function testDefaultValidator() + { + $p = $this->ns->getValue('p1'); + $this->assertEquals($p,'stringa'); + } + + + public function testDefaultValidatorWithArray() + { + $p = $this->ns->getValue('p4'); + $this->assertEquals($p,array(1,2,'ff')); + } + + /** + * @expectedException \Exception + */ + public function testDefaultValidatorWithUnsupportedTypeKO() + { + $p = $this->ns->getValue('p5',null,FILTER_VALIDATE_INT); + } + + + public function testENUM() + { + $p = $this->ns->getValue('p1',null, V::ENUM('stringa|string')); + $this->assertEquals($p,'stringa'); + } + + /** + * @expectedException \Exception + */ + public function testENUMKO() + { + $p = $this->ns->getValue('p1',null, V::ENUM('stringx|string')); + } + + + public function testWithCustomValidator() + { + $validator = new ValidatorStubOK; + $p = $this->ns->getValue('p1',null, $validator ); + $this->assertEquals($p,'stringa'); + } + + + /** + * @expectedException \Exception + */ + public function testWithCustomValidatorKO() + { + $validator = new ValidatorStubKO; + $p = $this->ns->getValue('p1',null, $validator ); + } + + + public function testSimpleFilterValidator() + { + $p = $this->ns->getValue('p2',null, FILTER_VALIDATE_INT); + $this->assertEquals($p,10); + } + + /** + * @expectedException \Exception + */ + public function testSimpleFilterValidatorKO() + { + $p = $this->ns->getValue('p1',null, FILTER_VALIDATE_INT); + } + + + public function testValidatorWithOptions() + { + $p = $this->ns->getValue('p2',null, + array('filter'=>FILTER_VALIDATE_INT,'options'=> array('min_range' => 5, 'max_range' => 15))); + $this->assertEquals($p,10); + } + + + /** + * @expectedException \Exception + */ + public function testValidatorWithOptionsKO() + { + $p = $this->ns->getValue('p2',null, + array('filter'=>FILTER_VALIDATE_INT,'options'=> array('min_range' => 11, 'max_range' => 15))); + + } + /** + * @expectedException \Exception + * + * Unsupported + */ + public function testSimpleFilterValidatorArray() + { + $p = $this->ns->getValue('p3',null, FILTER_VALIDATE_INT); + //$this->assertEquals($p,array(1,2,3)); + } + + /** + * @expectedException \Exception + * + * Unsupported + */ + public function testValidatorWithOptionsArray() + { + $p = $this->ns->getValue('p3',null, + array('filter'=>FILTER_VALIDATE_INT,'options'=> array('min_range' => 0, 'max_range' => 5))); + //$this->assertEquals($p,array(1,2,3)); + } + + + public function testOptionalPar() + { + $p = $this->ns->getValue('pnotexists',20 ,FILTER_VALIDATE_INT); + $this->assertEquals($p,20); + } + + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Missing mandatory + */ + public function testMandatoryPar() + { + $p1 = $this->ns->getValue('pnotexists',null); + } + +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 0000000..4edc198 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,22 @@ + + + + ../library/BOTK/ + + + + . + + +