<?php
defined( 'ABSPATH' ) or exit;   // show a blank page if try to access this file directly

require_once( EWZ_PLUGIN_DIR . 'classes/ewz-base.php' );

/*
 * Base class for input validation
 * Has a set of rules specifying the type of each allowed input
 * This base class has the code for validating standard types like
 * database sequence numbers and other integers, and items with
 * only specific allowed values.
 *
 * Child classes may specify validation rules for other types.
 */
abstract class Ewz_Input {
    // alpha-numeric, underscore or dash, starting with alpha
    const REGEX_ALPHA_NUM = '/^[A-Z][A-Z0-9_\-]*$/i';

    // allowed identifier - alpha-numeric plus _ or -
    const REGEX_IDENT = '/^[A-Z0-9_\-]+$/i';

    // integer >= 0
    const REGEX_SEQ   = '/^[0-9]+$/';

    // same as above, but used in different context, so may change later
    const REGEX_INT   = '/^[0-9]+$/';

    // integer >= -1
    const REGEX_INT1   = '/^[0-9]+$|-1/';

    // integer >= 1
    const REGEX_NZ_INT   = '/^[1-9][0-9]+$/';

    // 0 or 1
    const REGEX_BOOL  = '/^[01]$/';

   /**
    * @var array<mixed> $input_data;
    */
    protected $input_data = array();
   /**
    * @var array<mixed> $rules;
    */
    protected $rules;

    /**
     * @param array<mixed> $form_data
     *
     * @return void
     */
    function __construct( $form_data ) {
        if( !is_array( $form_data ) ){
            throw new EWZ_Exception("Invalid input");
        }
        $this->input_data = $form_data;
    }


    /**
     * @return boolean  true on success
     */
    public function validate( ) {
        // Validate each form field
        foreach ( $this->rules as $field => $specs ) {
            if ( $specs['req'] && ! isset( $this->input_data[$field] ) ) {
                throw new EWZ_Exception( get_class( $this ) . ": $field is required." );
            }
        }
        foreach ( $this->input_data as $field => $value ) {
            if( isset( $this->rules[$field] ) ){

                // rules[$field]['val'] is an array passed to the validating function.
                // Note php treats string '0' like '' in a number of places, so handle it carefully

                if ( $value == '' ){    // "0" == '' tests false
                    if ( $this->rules[$field]['type'] == 'noval' ) {
                        return true;
                    } elseif ( $this->rules[$field]['req'] ) {
                        throw new EWZ_Exception( get_class( $this ) . ": $field is required." );
                    }
                } else {
                    /**
                     * For each rule specified for an element,
                     * call a function with the same name, e.g. 'limited()' when
                     * checking whether a field has one of a given set of values
                     *
                     */
                    if ( $this->rules[$field]['type'] == 'to_bool' && $value == 'on' ){
                        $this->input_data[$field] = '1';
                    }
                    if ( $this->rules[$field]['type'] == 'to_bool' && $value == 'off' ){
                        $this->input_data[$field] = '0';
                    }
                    if ( ! call_user_func_array(
                                                array( $this, $this->rules[$field]['type'] ),   // function to call
                                                array( &$this->input_data[$field], $this->rules[$field]['val'] )    // args to pass
                                                ) ){
                        $class = str_replace( '_Input', '', str_replace( 'Ewz_', '', get_class( $this ) ) );
                        throw new EWZ_Exception( $class . " found invalid value for $field: " . $value );
                    }
                }
            } else {
                error_log( 'EWZ: ' . get_class( $this ) . ": unexpected input field $field with value '" . $value . "'" );
                unset( $this->input_data[$field] );
            }
        }
        return true;
    }

    /**
     * @return array<mixed>
     */
    public function get_input_data(){
       return $this->input_data;
   }

    /********* Validation Functions ************/

    /**
     * @param mixed $value
     * @param string $arg
     *
     * @return boolean
     */
    protected static function fixed( $value, $arg ) {
        return ( is_string( $value ) && ( $value == $arg ) );
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    protected static function limited( $value, $arg ) {
        return ( is_string( $value ) && ( in_array( $value, $arg ) ) );
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    // for values passed to functions that do their own validation
    protected static function arrayv( $value, $arg ) {
        return true;
    }

    /**
     * @param mixed $value
     * @param string $arg
     *
     * @return boolean
     */
    protected static function anonce( $value, $arg ) {
        return check_admin_referer( 'ewzadmin', 'ewznonce' );
    }

    /**
     * @param mixed $value
     * @param string $arg
     *
     * @return boolean
     */
    protected static function unonce( $value, $arg ) {
        return wp_verify_nonce( $value, 'ewzupload' );
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    protected static function ident( $value, $arg  ) {
        if(!is_string( $value ) ){
            return false;
        }
        if( isset( $arg['len'] ) &&  strlen( $value ) > $arg['len'] ){
            return false;
        }
        if( !preg_match( self::REGEX_IDENT, $value ) ){
             return false;
        }
        return true;
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    protected static function alpha_num( $value, $arg  ) {
        if(!is_string( $value ) ){
            return false;
        }
        if( isset( $arg['len'] ) &&  strlen( $value ) > $arg['len'] ){
            return false;
        }
        if( !preg_match( self::REGEX_ALPHA_NUM, $value ) ){
             return false;
        }
        return true;
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    protected static function to_seq( &$value, $arg  ) {
        if(!is_string( $value ) ){
            return false;
        }
        if( isset( $arg['len'] ) &&  strlen( $value ) > $arg['len'] ){
            return false;
        }
        if( !preg_match( self::REGEX_SEQ, $value ) ){
            return false;
        }
        $value = (int)$value;
        return true;
    }

    /**
     * Test for  undefined or '', allow  values of 0, "0"
     *
     * @param   mixed   $value
     * @return  boolean
     */
    protected static function is_blank( $value ){
        if( !isset( $value ) ){
            return true;
        }
        if( $value === NULL || $value === '' ){   /** @phpstan-ignore-line */
            return true;
        }
        if( is_array($value) && !$value ){
            return true;
        }
        return false;
    }

    /**
     * @param mixed $value
     * @param array<mixed> $arg
     *
     * @return boolean
     */
    protected static function to_int1( &$value, $arg  ) {
        if(!is_string( $value ) ){
            return false;
        }
        if( isset( $arg['len'] ) &&  strlen( $value ) > $arg['len'] ){
            return false;
        }
        if( !preg_match( self::REGEX_INT1, $value ) ){
            return false;
        }
        $value = (int)$value;
        return true;
    }

    /**
     * @param mixed   $value
     * @param array<mixed>   $arg
     * @param boolean $allow1
     *
     * @return boolean
     */
    protected static function is_int_arr( $value, $arg, $allow1  ) {

        if ( ! is_array( $value ) ){
            return false;
        }
        $c = count( $value );
        foreach ( $value as $val ){
            if ( ! is_string( $val ) ){
                return false;
            }
            if ( $allow1 ){
                if ( ! preg_match( self::REGEX_INT1, $val ) ){
                 return false;
                }
            } else {
                if ( ! preg_match( self::REGEX_INT, $val ) ){
                    return false;
                }
            }
        }
        if ( $c > $arg[1] || $c < $arg[0] ){
            return false;
        }
        return true;
    }

    /**
     * @param mixed   $value
     * @param array<mixed>   $arg
     *
     * @return boolean
     */
    protected static function int_arr( $value, $arg  ) {
        return self::is_int_arr( $value, $arg, false  );
    }

    /**
     * @param mixed   $value
     * @param array<mixed>   $arg
     *
     * @return boolean
     */
    protected static function int1_arr( $value, $arg  ) {
        return self::is_int_arr( $value, $arg, true  );
    }

    /**
     * @param mixed   $value
     * @param array<mixed>   $arg
     *
     * @return boolean
     */
    protected static function to_string( &$value, $arg  ) {
        if( is_string( $value ) ){
            // decode entities previously encoded for html display
            $value = html_entity_decode( $value );
            if ( isset( $arg['len'] ) ){
                return ( strlen( $value ) <= $arg['len'] );
            } else {
                return true;
            }
        } else {
            return false;
        }
    }

    /**
     * Return true if $value is a valid boolean as stored in the database,
     *       or a valid return from a checkbox with no value set.
     * Set $value to true or false
     * @param mixed   $value
     * @param mixed   $arg    unused
     *
     * @return boolean
     */
    protected static function to_bool( &$value, $arg=''  ) {
        if ( !( $value === '1'||$value === 1||$value === 'on'||$value === '0'||$value === 0||$value === 'off'  ) ){
            $value = false;
            return false;
        }
        if( $value === '1'||$value === 1||$value === 'on') {
            $value = true;
            return true;
        }
        if($value === '0'||$value === 0||$value === 'off')  {   /** @phpstan-ignore-line */
            $value = false;
            return true;
        }
        /** @phpstan-ignore-next-line */
        return false;
    }
}
