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.
+
+
+
+ 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.
+
+
+
+ Table of contents
+
+
+
+
+ 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:
+
+
+ the variables defined in the heading of the request and exposed by web server (INPUT_SERVER );
+ the variables encoded in http URI (INPUT_GET );
+ the variables encoded in http body (INPUT_POST );
+ the system environment (INPUT_ENV );
+ local defined variables (Context::LOCAL).
+
+
+ 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:
+
+ getString ($varName,$default = null): same as
+ getValue($varName,$default, null, FILTER_SANITIZE_STRING);
+ getURI ($varName,$default = null): same as
+ getValue($varName,$default, self::STRING('/.+/'), FILTER_SANITIZE_URL)
+
+
+
+ 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:
+
+ plabel : contains the variable that contains page num. For default 'page'
+ pslabel : contains the variable that contains page size. For default 'pagesize
+ pagesize : contains the default value for page size. The default is 100
+
+
+ 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/
+
+
+
+ .
+
+
+