1: <?php
2:
3: /**
4: * Description of HRC (HTTPRequestChecker)
5: *
6: * Class designed to check input parameters(HTTP request) using
7: * various validation rules.
8: *
9: * @author Radim SVOBODA <r.svoboda@pixvalley.com>
10: * @date 2012/12/14
11: */
12: class HRC
13: {
14: const BOOL=1,
15: EMAIL=2,
16: FLOAT=3,
17: INT=4,
18: IP=5,
19: REGEXP=6,
20: URL=7
21: //LENGTH=8,
22: //COUNT=9
23: ;
24:
25: protected $globalToCheck;// = "REQUEST";
26: protected $inputParams,
27: $currentInput
28: ;
29:
30: //protected $inputDefaults = array();
31:
32: public function __construct()
33: {
34: $this->reset();
35:
36: $this->setRequestMethod();
37: }
38: public function reset()
39: {
40: $this->inputParams = array();
41: $this->currentInput = null;
42:
43:
44: //$this->inputDefaults = array();
45:
46: return $this;
47: }
48: /**
49: * Clones the current object, runs the reset() method and returns the object
50: *
51: * @return \HRC the newly created/cloned object
52: */
53: public function newInstance()
54: {
55: //$o = new $this;
56: $o = clone $this;
57: return $o->reset();
58: }
59: /**
60: * Compares two operands with a specified operator
61: *
62: * @static
63: * @param type $x operand
64: * @param type $y operand
65: * @param type $operator string representation of comparison operator.
66: * Supported are: "==", "===", "!=", "!==", ">", ">=", "<", "<="
67: * @return boolean the comparison result; If operator is not supported, NULL
68: */
69: public static function cmp($x,$y,$operator="==")
70: {
71: switch($operator):
72: case "==": return ($x==$y);
73: case "===": return ($x===$y);
74: case "!=": return ($x!=$y);
75: case "!==": return ($x!==$y);
76: case ">": return ($x>$y);
77: case ">=": return ($x>=$y);
78: case "<": return ($x<$y);
79: case "<=": return ($x<=$y);
80: endswitch;
81: //operator not found
82: return null;
83: }
84: /**
85: * Based on the first parameter which is a rule/callback (int/callable)
86: * the method createHRCRule or createCustomRule is called. Check those
87: * methods for more information on which parameters to pass.
88: *
89: * @static
90: * @param callable|int $mRule
91: * @return array created rule
92: */
93: public static function createRule($mRule)
94: {
95: $aArgs = func_get_args();
96: $staticMethod = is_int($mRule) ? "createHRCRule" : "createCustomRule";
97:
98: return call_user_func_array(array(__CLASS__, $staticMethod), $aArgs);
99: }
100:
101: /**
102: *
103: * @param callable $cRule
104: * @param array $aArgs
105: * @param int $iArgIndex optional
106: * @param string $sCompareOperator
107: * @param mixed $mCompareValeu
108: * @return array
109: */
110: public static function createCustomRule($cRule,
111: $aArgs=array(), $iArgIndex=null,
112: $sCompareOperator="==", $mCompareValeu=true)
113: {
114: $a = array(
115: "fn"=>$cRule, "args"=>$aArgs, "argIndex"=>$iArgIndex,
116: // "equalsVal"=>$bEqualsTo, "strictEqual"=>$bStrictEqual
117: "cmpOp"=>$sCompareOperator, "cmpVal"=>$mCompareValeu
118: );
119: if( is_string( $iArgIndex) )
120: {//$iArgIndex is missing; should be set to NULL(==appending)
121: $a["cmpVal"] = $a["cmpOp"];
122: $a["cmpOp"] = $a["argIndex"];
123: $a["argIndex"] = null;
124: }
125:
126: return $a;
127: }
128:
129: /**
130: * Creates a rule based on the arguments. Mostly uses the function
131: * filter_var. Using this method it is easier to pass arguments than it
132: * would be in createCustomRule using "filter_var" as callable.
133: * Mostly, all the arguments, except the first one, are optional
134: *
135: * <code>
136: * <pre>
137: * //chcecking for boolean using filter_var validation filter
138: * ->createHRCRule(HRC::BOOL, array|int $flag)
139: *
140: * //chcecking for integer using filter_var validation filter
141: * //if you e.g. want to set only max. value, set min. val. to NULL
142: * ->createHRCRule(HRC::INT, int $iMin=null, int $iMax=null, array|int $flag)
143: *
144: * //chcecking for float using filter_var validation filter
145: * ->createHRCRule(HRC::FLOAT, string $sDecimal=null, array|int $flag)
146: *
147: * //chcecking for IP using filter_var validation filter
148: * ->createHRCRule(HRC::IP, array|int $flag)
149: *
150: * //chcecking for URL using filter_var validation filter
151: * ->createHRCRule(HRC::URL, array|int $flag)
152: *
153: * //chcecking for e-mail using filter_var validation filter
154: * ->createHRCRule(HRC::EMAIL)
155: *
156: * //chcecking if the input parameter matches regular expression
157: * //Of course, the $sRegExp is mandatory
158: * ->createHRCRule(HRC::REGEXP, $sRegExp)
159: * </pre>
160: * </code>
161: *
162: * @static
163: * @param int $iRule class constant indicating which rule to use
164: * @param mixed $arg1
165: * @param mixed $arg2
166: * @param mixed $arg3
167: * @return array|null
168: */
169: public static function createHRCRule($iRule,
170: $arg1=null, $arg2=null, $arg3=null )
171: {
172: //mostly filter_var function is used which needs specific options
173: $aOptions = array();
174: //value indicating failure
175: $mFailureValue=false;
176: //compares filter_var result with the failure value
177: $sCmpOperator = "!==";
178: //indicating the retrieved param should be the first argument passed
179: //to filter_var
180: $iParamIndex = 0;
181:
182: switch($iRule):
183:
184: /*case self::LENGTH:
185: case self::COUNT:
186:
187: throw new Exception("not implemented");*/
188:
189: case self::BOOL:
190: if($arg1!==null)//flags
191: {
192: $aOptions["flags"]= is_array($arg1) ? $arg1 : array($arg1);
193:
194: if( in_array( FILTER_NULL_ON_FAILURE, $aOptions["flags"], true ))
195: $mFailureValue=null;
196: }
197: return self::createCustomRule(
198: "filter_var",
199: array(FILTER_VALIDATE_BOOLEAN, $aOptions),$iParamIndex,
200: $sCmpOperator, $mFailureValue
201: );
202: case self::INT:
203: if($arg1!==null)
204: {
205: $aOptions["min_range"]=$arg1;
206: }
207: if($arg2!==null)
208: {
209: $aOptions["max_range"]=$arg2;
210: }
211: if( count( $aOptions) > 0 )
212: {//if there are any values, put them at the "right place"
213: $aOptions = array("options"=>$aOptions);
214: }
215: if($arg3!==null)//flags
216: {
217: $aOptions["flags"]= is_array($arg3) ? $arg3 : array($arg3);
218: }
219: return self::createCustomRule(
220: "filter_var",
221: array(FILTER_VALIDATE_INT, $aOptions), $iParamIndex,
222: $sCmpOperator, $mFailureValue
223: );
224: case self::FLOAT:
225: if($arg1!==null)//decimal (string character)
226: {
227: $decimal = array("decimal"=>$arg1);
228: $aOptions["options"]=$decimal;
229: }
230: if($arg2!==null)//flags
231: {
232: $aOptions["flags"]= is_array($arg2) ? $arg2 : array($arg2);
233: }
234: return self::createCustomRule(
235: "filter_var",
236: array(FILTER_VALIDATE_FLOAT, $aOptions), $iParamIndex,
237: $sCmpOperator, $mFailureValue
238: );
239:
240: case self::IP:
241: if($arg1!==null)//flags
242: {
243: $aOptions["flags"]= is_array($arg1) ? $arg1 : array($arg1);
244: }
245: return self::createCustomRule(
246: "filter_var",
247: array(FILTER_VALIDATE_IP, $aOptions), $iParamIndex,
248: $sCmpOperator, $mFailureValue
249: );
250: case self::URL:
251: if($arg1!==null)//flags
252: {
253: $aOptions["flags"]= is_array($arg1) ? $arg1 : array($arg1);
254: }
255: return self::createCustomRule(
256: "filter_var",
257: array(FILTER_VALIDATE_URL, $aOptions), $iParamIndex,
258: $sCmpOperator, $mFailureValue
259: );
260: case self::EMAIL:
261: return
262: self::createCustomRule( "filter_var",
263: FILTER_VALIDATE_EMAIL, $iParamIndex,
264: $sCmpOperator, $mFailureValue
265: );
266: case self::REGEXP:
267: //without regexp passed, it doesnt have sense...
268: //so there is no checking for it
269: $aOptions = array("options"=>array("regexp"=>$arg1));
270: return self::createCustomRule( "filter_var",
271: array(FILTER_VALIDATE_REGEXP, $aOptions), $iParamIndex,
272: $sCmpOperator, $mFailureValue
273: );
274:
275: endswitch;
276:
277: return null;
278: }
279: /*public function isValid()
280: {
281: return true;
282: }*/
283:
284: /**
285: *
286: * @param string $sType of request, e.g. REQUEST, POST, GET
287: * @return \HRC the object instance to support chanability
288: */
289: public function setRequestMethod($sType="REQUEST")
290: {
291: $sType = strtoupper($sType);
292: if( $sType=="REQUEST")
293: $this->globalToCheck =&$_REQUEST;
294: else
295: $this->globalToCheck =&$GLOBALS["_$sType"];
296:
297: return $this;
298: }
299:
300: /**
301: * Stores the information about a desired value in superglobals to check
302: *
303: * @param string|array $key key of value we want to check. When array is
304: * e.g. array("cat","col") it would check for instance $_POST["cat"]["col"]
305: * @param string $desiredName name you want to use within HRC. It is
306: * mandatory when $key is an array
307: * @return \HRC instance
308: */
309: public function check($key, $sDesiredName=null)
310: {
311: $name = empty($sDesiredName) ? $key : $sDesiredName ;
312: $this->inputParams[$name] = array("key"=>$key);
313: $this->currentInput = $name;
314:
315: return $this;
316: }
317:
318: public function useSettingFor($key, $sDesiredName=null)
319: {
320: $name = empty($sDesiredName) ? $key : $sDesiredName ;
321: $aSetting = $this->_get();//obtained by reference...
322: $this->inputParams[$name] = $aSetting;
323: $this->inputParams[$name]["key"] = $key;
324: $this->currentInput = $name;
325:
326: return $this;
327: }
328: /**
329: *
330: * @param callable $callable callback to execute
331: * @param type $value value we want to pass to the callback. Thi value is
332: * usually the one from superglobals
333: * @param array $aArgs list of additional arguments passed to the callback.
334: * If $aArgs is not an array, it is automatically wrapped into one.
335: * @param int $iValIndex zero-based position index of $value in the $aArgs.
336: * If null, $value is appended to $aArgs
337: * @return mixed callback return value
338: * @throws Exception in case an exception from callback is thrown, it is
339: * re-thrown
340: */
341: protected function _callFn($callable, $value, $aArgs=array(), $iValIndex=null)
342: {
343: if( !is_array($aArgs) )
344: $aArgs=array($aArgs);
345:
346: if($iValIndex===null)
347: {//append
348: $aArgs[]=$value;
349: }else{ //put the obtained value at the right position
350: $aArgs = array_merge(
351: array_slice($aArgs, 0, $iValIndex),
352: array($value),
353: array_slice($aArgs, $iValIndex) );
354: }
355:
356: try
357: {
358: return call_user_func_array($callable, $aArgs);
359: }catch(Exception $ex){
360: throw $ex; //just re-throw
361: }
362: }
363: /**
364: * Gets a value from a superglobal
365: *
366: * @param string|array $key key of variable|array of keys for e.g.
367: * $_POST['category']['name']
368: * @param string|array $default value to return if the searched var
369: * is not found
370: * @return string|array retrieved varible; by reference
371: * @throws HRCException if the var doesn't exist, and no default value is set
372: */
373: protected function &_getFromGlobal($key, $default=null)
374: {
375: if( !is_array($key) )
376: $key=array($key);
377: $keyString = join("][",$key);
378: //create var to access the global scope easily
379: $glob =& $this->globalToCheck;
380: while( ( $k = array_shift($key)) )
381: {
382: if( isset($glob[$k]) )
383: {
384: $glob=&$glob[$k];
385: }
386: else if( $default!==null )
387: {
388: return $default;
389: }
390: else
391: {
392: throw new HRCException(
393: "[$keyString] not found",
394: HRCException::NOT_FOUND,
395: $key
396: );
397: }
398: }
399: return $glob;
400: }
401: /**
402: * Gets the parameter setting
403: *
404: * @param string $sName name of the parameter
405: * @return array setting for a parameter. The current parameter is returned
406: * if no parameter spiceifed. NULL if not found.
407: */
408: protected function &_get($sName=null)
409: {
410: $name = ($sName===null) ? $this->currentInput : $sName;
411:
412: /*if(empty($this->inputParams[$name]))
413: return null;
414:
415: return $this->inputParams[$name];*/
416: $null = null;
417: $a = array();
418: if( empty($this->inputParams[$name]) )
419: {
420: return $null;
421: }
422: else
423: {
424: return $this->inputParams[$name];
425: }
426:
427: }
428: public function setCurrent($sName)
429: {
430: $this->currentInput = $sName;
431: return $this;
432: }
433: public function get($name)
434: {
435: //get the settings
436: $param;
437: if( isset($this->inputParams[$name]) )
438: $param =& $this->inputParams[$name];
439: else
440: throw new HRCException(
441: "'$name' not defined",
442: HRCException::NOT_DEFINED
443: );
444: $value;
445: try
446: {
447: $mDefault = isset($param['default']) ? $param['default'] : null;
448: $value = $this->_getFromGlobal($param["key"], $mDefault);
449: /*if(isset($param['default']))//default value set?
450: {
451: $value = $this->_getFromGlobal($param["key"], $param['default']);
452: }
453: else
454: {
455: $value = $this->_getFromGlobal($param["key"]);
456: }*/
457: //Validation rules
458: if(isset($param['rules']))
459: {
460: foreach($param['rules'] as $aRule)
461: {
462: if( ! $this->_validate( $value, $aRule ))
463: {
464: throw new HRCException(
465: "'$name' has invalid format",
466: HRCException::NOT_VALID,
467: array("VALUE"=>$value, "RULE"=>$aRule));
468: }
469: }
470: }
471: }
472: catch(Exception $ex)
473: {
474: throw $ex;
475: }
476:
477: return $value;
478:
479: }
480: //TODO: test it!
481: public function getAll($returnArrayObject=false)
482: {
483: $aResult=array();
484: $aNames = array_keys($this->inputParams);
485: foreach($aNames as $key)
486: {
487: try
488: {
489: $aResult[$key] = $this->get($key);
490: }
491: catch(Exception $ex)
492: {
493: throw $ex;
494: }
495:
496: }
497:
498: //add conversion to object if needed
499: if( $returnArrayObject )
500: $aResult = new ArrayObject ( $aResult, ArrayObject::ARRAY_AS_PROPS);
501:
502: return $aResult;
503:
504: }
505: /**
506: * In case a variable is not set (!isset()) but we don't want any error to
507: * occur. We can set a default value which is used in case the desired
508: * parameter is not set.
509: *
510: * @param string|array $value the default value for a parameter if it is not
511: * set. Note that it has to be a string or possibly an array.
512: * More specifically, the value NULL will not work.
513: * @param string $sName name of the parameter in \HRC. If it is not set, the
514: * last one/current one is used
515: * @return \HRC instance
516: * @throws HRCException
517: */
518: public function setDefault($value, $sName=null)
519: {
520: $name = ($sName===null) ? $this->currentInput : $sName;
521: if( empty($this->inputParams[$name]) )
522: {
523: throw new HRCException(
524: "Param '$name' not set",
525: HRCException::NOT_DEFINED,
526: array($value, $sName)
527: );
528: }
529:
530: $this->inputParams[$name]['default'] = $value;
531:
532: return $this;
533: }
534: /**
535: * The method adds a new rule to used to validate input parameter.
536: * Based on the arguments, to create a new rule, a static method
537: * "createCustomRule" or "createHRCRule" is used to generate the actual rule.
538: * Then, it is saved inside our object. For more information see those
539: * methods
540: *
541: * @see HRC::createHRCRule()
542: * @TODO comment HRC::createCustomRule
543: *
544: * @param mixed $mRule
545: * @param type $aArgs
546: * @param type $iArgIndex
547: * @param type $sCompareOperator
548: * @param type $mCompareValue
549: * @return \HRC instance
550: */
551: public function addRule($mRule, $aArgs=array(), $iArgIndex=null,
552: $sCompareOperator="==", $mCompareValue=true//,$sName=null
553: )
554: {
555: //$name = ($sName===null) ? $this->currentInput : $sName;
556: //$param =& $this->_get( );//
557: //create an array of rules if not any
558: //if( !isset($param["rules"]) )
559: // $param["rules"]=array();
560: //create the rule
561:
562: $aRule;
563: if( is_int($mRule) )
564: {//use only arguments that are passed by user/programmer...
565: $aRule = call_user_func_array( array(__CLASS__, "createHRCRule"),
566: func_get_args()
567: );
568: }
569: else
570: { //use all the parametrs including the default values...
571: $aRule = self::createCustomRule(
572: $mRule, $aArgs, $iArgIndex, $sCompareOperator, $mCompareValue);
573: }
574: //store the rule
575: return $this->addMultiRule($aRule, false);
576: //$param["rules"][] = array( $aRule );
577:
578: // return $this;
579:
580: }
581: //TODO fix the params
582: public function addArrayItemRule($cRule, $aArgs=array(), $iArgIndex=null,
583: $sCompareOperator="==", $mCompareValue=true//,$sName=null
584: )
585: {
586: $aRule;
587: if( is_int($cRule) )
588: {//use only arguments that are passed by user/programmer...
589: $aRule = call_user_func_array( array(__CLASS__, "createHRCRule"),
590: func_get_args()
591: );
592: }
593: else
594: { //use all the parametrs including the default values...
595: $aRule = self::createCustomRule(
596: $cRule, $aArgs, $iArgIndex, $sCompareOperator, $mCompareValue);
597: }
598:
599:
600: //store the rule
601: return $this->addMultiRule($aRule, true);
602: //return $this;
603: }
604: public function addMultiRule( array $aRule1, $aRuleN=null,
605: $bCheckArrayItems=false)//, $sName=null
606: {
607: $aRules = func_get_args();//array of arrays
608:
609: if( !is_array( $aRules[count($aRules)-1] ) )//last arg is not aRule
610: { //indicates if we should check all array items
611: $bArrayItems =array_pop($aRules);
612: if( $bArrayItems )
613: {
614: foreach( $aRules as &$aRule)
615: {//set the attribute indicating we should check every item in
616: //an array
617: $aRule["checkArrayItems"] = true;
618: }
619: }
620: }
621: //get the current param setting
622: $param =& $this->_get();
623: if($param===null)
624: return null;
625: //create an array of rules if not any
626: if( !isset($param["rules"]) )
627: $param["rules"]=array();
628:
629: $param["rules"][] = $aRules;
630: //var_dump( array("PARAM"=>$param, "NAME"=>$this->inputParams[$name],"--------------------------------------------------------------------------" ));
631: return $this;
632:
633: }
634: protected function _validateString($string, $aRule)
635: {
636: $returnVal = $this->_callFn(
637: $aRule["fn"], $string,
638: $aRule["args"], $aRule["argIndex"]
639: );
640: //Is the $returnVal ok?
641: return self::cmp( $returnVal, $aRule["cmpVal"], $aRule["cmpOp"] ) ;
642:
643: }
644: protected function _validateArray($array, $aRule)
645: {
646: if( is_array($array) )
647: {
648: foreach($array as $item)
649: { //each element needs to pass the rules
650: if( ! $this->_validateString($item, $aRule))
651: {
652: //echo "||IVALID $item::";
653: return false;
654: }
655: }
656: return true;
657: }
658: else
659: { //$array is not array
660: //echo"NOT ARRAY";
661: return false;
662: }
663:
664:
665: }
666: protected function _validate($value, $aRule)
667: {
668:
669:
670: try
671: {
672: foreach($aRule as $aOneRule)
673: {
674: //should we check each array item?
675: if( ! empty($aOneRule["checkArrayItems"]) )
676: {
677: //var_dump( $value, $aOneRule );
678: if( $this->_validateArray($value, $aOneRule))
679: return true;//at least one rule qualifies
680: }
681: else
682: {
683: if( $this->_validateString($value, $aOneRule))
684: return true;//at least one rule qualifies
685: }
686:
687: }
688:
689:
690: //----------------------------
691: /* if( $isArray )
692: {
693: if( ! is_array($value) )
694: return false;
695:
696: foreach($value as $arrayItem)
697: {//one invalid item means it's all wrong...
698: //recursive call
699: if ($this->_validate($arrayItem, $aRule, false)===false)
700: {
701: //throw new Exception(print_r(array($arrayItem, $aRules, false)));
702: return false;
703: }
704: }
705: return true;//the whole array is ok
706:
707: }
708: else
709: foreach($aRule as $aOneRule)
710: {
711: $returnVal = $this->_callFn(
712: $aOneRule["fn"], $value,
713: $aOneRule["args"], $aOneRule["argIndex"]
714: );
715: //$aRule["cmpVal"]$aRule["cmpOp"]
716: if( self::cmp( $returnVal, $aOneRule["cmpVal"], $aOneRule["cmpOp"] ) )
717: return true;
718:
719: if(empty($aRule["strictEqual"]))
720: {
721: if($returnVal == $aRule["equalsVal"])
722: return true;//at least one rule qualifies
723: }
724: else
725: {
726: if($returnVal === $aRule["equalsVal"])
727: return true;//at least one rule qualifies
728: }
729:
730: }*/
731: }catch(Exception $ex){
732: throw $ex;
733: }
734: return false;
735: }
736:
737: /* protected function _validateString()
738: {
739:
740: }//*/
741:
742: }
743:
744:
745:
746:
747: class HRCException extends Exception
748: {
749: const NOT_FOUND=100,//for wrongly specified superglobal
750: NOT_DEFINED=110,
751: NOT_VALID=200,
752: NOT_ARRAY=210,//supplied argument is not an array, ut should be
753: CALLBACK=300
754: ;
755: /* Array containing data that could be useful for debuging */
756: protected $aDev;// = array();
757: // Redefine the exception so message isn't optional
758: public function __construct($message, $code = 0, $aDev=array()) {
759: // some code
760: $this->aDev = $aDev;
761: // make sure everything is assigned properly
762: parent::__construct($message, $code);
763: }
764:
765: // custom string representation of object
766: public function __toString() {
767: return __CLASS__ . ": [{$this->code}] {$this->message}\n";
768: }
769:
770: public function getDevInfo() {
771: return $this->aDev;
772: }
773: }
774:
775:
776: