InputValidator.php

<?php

namespace Lia\Utility;

/**
 * Simple class for validating user inputs
 */
class InputValidator {

    protected array $rules = [];

    /**
     *
     * @param $rules_list either string separated by bars: `rule1|rule2|rule3` or `array<string rule_name, value rule_paramaters>`
     * @param $input_value the value being validated
     * @param $input_name the name of the input being validated (*built-in rules don't use this*)
     * @param $ref a variable whose value will be set to `$input_value` and may be changed by validation functions
     */
    public function validate(array|string $rules_list, mixed $input_value, ?string $input_name = null, mixed &$ref = null): bool {

        if (is_string($rules_list)){
            $rules_array = array_fill_keys(explode("|", $rules_list), null);

            //var_dump($rules_array);
            foreach ($rules_array as $key=>$nullvalue){
                $parts = explode(":", $key);
                if (count($parts)==1)continue;
                unset($rules_array[$key]);
                $rules_array[array_shift($parts)] = implode(':',$parts);
            }
        } else {
            $rules_array = $rules_list;
        }

        foreach ($rules_array as $rule_name => $rule_options){
            $is_valid = false;
            if (is_int($rule_name)){
                $rule_name = $rule_options;
                $rule_options = null;
            }
            if (isset($this->rules[$rule_name])){
                $callable = $this->rules[$rule_name];
                $is_valid = $callable($input_value, $input_name, $rule_name, $rule_options);
            } else if (method_exists($this, $mname="is_".$rule_name)){
                $is_valid = $this->$mname($input_value, $rule_options, $input_name, $rule_name);
            } else {
                throw new \Lia\Exception(\Lia\Exception::VALIDATION_RULE_DOESNT_EXIST, $rule_name);
            }

            if ($is_valid !== true)return false;
        }


        $ref = $input_value;
        return true;
    }

    /**
     * Add a rule
     *
     * @param string $rule_name 
     * @param $callable `function(mixed $value_to_validate, string $name_of_input, string $rule_name, ?mixed $rule_options): bool;` Your function returns true if the input is valid
     *
     */
    public function add_rule(string $rule_name, callable $callable, bool $overwrite_existing = false){
        if ($this->has_rule($rule_name)){
            if ($overwrite_existing === false){
                throw new \Lia\Exception(\Lia\Exception::VALIDATION_RULE_EXISTS, $rule_name);
            }         
        }
        $this->rules[$rule_name] = $callable;
    }

    public function has_rule(string $rule_name): bool {
        if (isset($this->rules[$rule_name])
            || method_exists($this, 'is_'.$rule_name)){
            return true;
        }
        return false;
    }


    public function get_failure_message(string $rule_name): string|bool {
        if (!method_exists($this, 'is_'.$rule_name)){
            throw new \Lia\Exception(\Lia\Exception::VALIDATION_METHOD_NOT_EXISTS, $rule_name);
        }

        $rm = new \ReflectionMethod($this, 'is_'.$rule_name);
        $doc = $rm->getDocComment();
        $pos = strpos($doc,"@failmsg");
        if ($pos === false) return false;

        $msg = substr($doc, $pos + strlen("@failmsg"));
        $msg = substr($msg, 0, strpos($msg, "\n"));

        return trim($msg);
    }

    /** $value must be a string per `is_string($value)` */
    public function is_string(mixed $value){
        if (is_string($value))return true;
        return false;
    }

    /** $value must be a number per `is_numeric($value)` */
    public function is_number(mixed $value){
        return is_numeric($value);
    }
    /** $value must be an int. Typecasts to int if value is a string containing int. */
    public function is_int(mixed &$value){
        if (is_int($value))return true;
        else if (is_string($value)){
            $intval = (int)$value;
            $stringval = (string)$intval;
            if ($stringval === $value){
                $value = $intval;
                return true;
            }
        }
        return false;
    }

    /** $value must be alphanumeric, per `ctype_alnum($value)` */
    public function is_alnum(mixed $value){
        return ctype_alnum($value);
    }

    /** $value must be in the $target_values list. $target_values are comma separated */
    public function is_oneof(mixed $value, string $target_values){
        $options = explode(",", $target_values);
        if (in_array($value, $options))return true;
        return false;
    }

    /** $value must be a string longer than $length */
    public function is_maxlen(mixed $value, string $length){
        $length = (int)$length;
        if (strlen($value) <= $length)return true;

        return false;
    }
    /** $value must be >= $intval */
    public function is_minval(mixed $value, string $intval){
        $length = (int)$intval;
        if ($value >= $intval)return true;

        return false;
    }
    /** (UNTESTED) $value must not contain html or htmlspecialchars. If you pass 'strip', then `strip_tags()` & `htmlspecialchars()` will modify value.
     */
    public function is_nohtml(mixed &$value, string $should_strip){
        if ($should_strip == 'strip'){
            $value = strip_tags($value);
            $value = htmlspecialchars($value);
            return true;
        }

        $stripped = strip_tags($value);
        $stripped = htmlspecialchars($value);
        if ($stripped === $value)return true;

        return false;
    }

    /** (UNTESTED) $value must only contain safe html tags. If you pass 'strip', then value is set to `strip_tags($value, ALLOWED_TAGS)`. (*no script or style. See function definition for full list*)
     *
     * @failmsg Some disallowed HTML tags were present. h1-h6, p, div, span, summary/details, small, strike, and other basic formatting tags are available. 
     */
    public function is_safehtml(mixed &$value, string $should_strip){
        $safe_tags = [
            'div','span','p','br','ul','ol','li','pre',
            'h1','h2','h3','h4','h5','h6',
            'address','blockquote','bold','strong',
            'i','center','code','hr','a','details',
            'summary','small','strike','sub','sup','u',
        ];
        $stripped = strip_tags($value, $safe_tags);
        if ($should_strip == 'strip'){
            $value = $stripped;
            return true;
        }

        if ($value == $stripped)return true;

        return false;
    }
}