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

require_once( EWZ_PLUGIN_DIR . "classes/validation/ewz-input.php");
require_once( EWZ_PLUGIN_DIR . "classes/ewz-layout.php");

class Ewz_Upload_Input extends Ewz_Input
{
    protected  $layout;

    // usage: $input = new Ewz_Upload_Input( stripslashes_deep( $_POST ), $_FILES, $webformdata['layout'] );

    /**
     * @param array<mixed>      $form_data
     * @param array<mixed>      $in_files
     * @param Ewz_Layout $in_layout
     *
     * @return void
     */
    function __construct( $form_data, $in_files, $in_layout ) {
        parent::__construct( $form_data );

        $this->layout = $in_layout;

        $max_rows = 100;

        // Set the rdata value for the image upload fields ( should have been done by js anyway )
        // Needs to be done before validation
        if ( $in_files ) {
            if( !(    is_array($in_files)            && array_key_exists('rdata', $in_files )
                   && is_array( $in_files['rdata'] ) && array_key_exists('tmp_name', $in_files['rdata'] )
                                                     && array_key_exists('error', $in_files['rdata'] )
                                                     && array_key_exists('type', $in_files['rdata'] )
                  && is_array( $in_files['rdata']['type'])
                  && is_array( $in_files['rdata']['name'])
                  ) ) {
                throw new EWZ_Exception(__("Invalid format for file upload.",'entrywizard'));
            }

            foreach ( $in_files['rdata']['name'] as $row => $uploaded_fileinfo ) {
                if( !( is_int($row) && ($row < $max_rows) && is_array($uploaded_fileinfo) ) ){
                     throw new EWZ_Exception(__("Invalid specification for an uploaded file.",'entrywizard'));
                }
                foreach ( $uploaded_fileinfo as $field_id => $filename ) {
                    if( !( is_int($field_id) && is_string($filename) ) ){
                        throw new EWZ_Exception(__("Invalid value for file-upload field.",'entrywizard'));
                    }
                    if ( $filename ) {
                        $this->input_data['rdata'][$row][$field_id] = 'ewz_img_upload';
                    }
                }
            }
        }

        // rdata1 is handled differently in javascript, but here we want to just treat it as
        // setting the same value for each item. Then we can apply restrictions to it.
        if( isset( $this->input_data['rdata1'] ) ){
            foreach( $this->input_data['rdata1'] as $field_id => $topdata ){
                foreach( $this->input_data['rdata'] as $n => $data ){
                    $this->input_data['rdata'][$n][$field_id]  = $this->input_data['rdata1'][$field_id];
                }
            }
            unset( $this->input_data['rdata1'] );
        }
        $this->rules = array(
            'webform_id'       => array( 'type' => 'to_seq',    'req' => true,  'val' => ['len' => 10 ] ),
            'ewzuploadnonce'   => array( 'type' => 'unonce',    'req' => true,  'val' => '' ),
            '_wp_http_referer' => array( 'type' => 'to_string', 'req' => false, 'val' => '' ),
            'rdata'            => array( 'type' => 'v_rdata',   'req' => true,  'val' => '' ),
            'item_id'          => array( 'type' => 'v_items',   'req' => false, 'val' => ['len' => 20 ] ),
            'identifier'       => array( 'type' => 'ident',     'req' => true,  'val' => ['len' => 15] ),
                             );
        $this->validate();
    }


  //****** All v_.... functions must return a boolean **************/

/**
 * Validate "item_id" input
 *
 * @param   mixed $value  input value, may be changed by function
 * @param   array<mixed> $arg    '' -- unused here, needed for generic code in ewz-input.php
 * @return  boolean
 */
  protected function v_items( &$value, $arg ){
      $ok = true;
      foreach ( array_keys( $value ) as $id ){
          $ok = $ok && self::to_seq( $value[$id], $arg  );      // seq potentially changes first arg
      }
      return $ok;
  }

/**
 * Validate "rdata" input
 *
 * Calls the static function
 * @param   mixed    $value  input value, may be changed by function
 * @param   string   $arg    '' -- unused here, needed for generic code in ewz-input.php
 * @return  boolean  true  ( exception raised if not valid )
 */
  public function v_rdata( &$value, $arg ){
      if( !is_array($value) ){
          throw new EWZ_Exception( __("Invalid input for rdata", 'entrywizard'));
      }
      return self::check_rdata($value, $this->layout, 0 );
  }


/**
 * Validate "rdata" input
 *
 * Static so it can be called to check batch uploads
 * @param   mixed       $value  input value, may be changed by function
 * @param   Ewz_Layout  $layout
 * @param   integer     $nitems used only for checking batch input
 * @return  boolean     true  ( exception raised if not valid )
 */
   public static function check_rdata( &$value, $layout, $nitems=0 ){

        $fields = $layout->fields;
        $optcounts = array();
        $chkcount = array();
        foreach ( $value as $rownum => $input_row ) {
            $rownum = (int)$rownum;
            $rownum1 = $rownum + 1;
            if( $nitems && $nitems < $rownum1 ){
                throw new EWZ_Exception( sprintf(__("Row %d: Too many items, only %d allowed.", 'entrywizard'),$rownum, $nitems) );
            }
            foreach ( $layout->restrictions as $restr ) {
                $row_matches_restr[$restr['msg']] = true;   // may set to false during check below
            }
            foreach ( $input_row as $field_id => $val ) {
                if ( !isset( $fields[$field_id] ) ){
                    throw new EWZ_Exception( sprintf(__("Row %d: invalid data", 'entrywizard'), $rownum ));
                }
                if ( self::is_blank($val) && $fields[$field_id]->required ) {
                    throw new EWZ_Exception( sprintf(__("%s is required ( row %d )", 'entrywizard'), $fields[$field_id]->field_header, $rownum1 ) );
                }

                // for counts, treat them all the same way
                // initialize to 0, then validate ( which may change things ), then increment
                // so validation must assume the input count is the count before the new item is added
                switch ( $fields[$field_id]->field_type ) {
                case 'str':
                        self::validate_str_data( $fields[$field_id]->fdata, $value[$rownum][$field_id] );  // may change 2nd arg
                        break;
                case 'lab':
                        self::validate_str_data( $fields[$field_id]->fdata, $value[$rownum][$field_id] );
                        break;
                case 'opt':
                        if( !isset( $optcounts[$field_id][$val] ) ){
                            $optcounts[$field_id][$val] = 0;
                        }
                        self::validate_opt_data( $fields[$field_id], $val, $optcounts[$field_id][$val] );
                        ++$optcounts[$field_id][$val];
                        break;
                case 'img':
                        self::validate_img_data( $val );
                        break;
                case 'rad':
                    if( !isset( $chkcount[$field_id] ) ){
                        $chkcount[$field_id] = 0;
                    }
                    $value[$rownum][$field_id] = self::validate_rad_data( $val, $chkcount[$field_id] );
                    if( $value[$rownum][$field_id] ){
                        ++$chkcount[$field_id];
                    }
                    break;
                case 'chk':
                    if( !isset( $chkcount[$field_id] ) ){
                        $chkcount[$field_id] = 0;
                    }
                    $value[$rownum][$field_id] = self::validate_chk_data( $fields[$field_id],
                                                                          $val,
                                                                          $chkcount[$field_id]
                                                                          );
                    if( $value[$rownum][$field_id] ){
                        ++$chkcount[$field_id];
                    }
                    break;
                default:
                    throw new EWZ_Exception( sprintf( __("Invalid field type '%s'",'entrywizard') , $fields[$field_id]->field_type ));
                }
                foreach ( $layout->restrictions as $restr ) {
                    $ismatch = self::field_matches_restr( $field_id, $restr, $val );
                    $row_matches_restr[$restr['msg']] = $row_matches_restr[$restr['msg']] && $ismatch; /** @phpstan-ignore-line */
                }
            }
            // check restrictions
            $bad_data = '';
            foreach ( $layout->restrictions as $restr ) {
                if ( $row_matches_restr[$restr['msg']] ) {   /** @phpstan-ignore-line */
                    $user_row = $rownum + 1;
                    $bad_data .= sprintf( __(" row %d: ", 'entrywizard'), $user_row ) . $restr['msg'];
                }
            }
            if ( $bad_data ) {
                throw new EWZ_Exception(  __("Restrictions not satisfied: ", 'entrywizard') . $bad_data );
            }
        }
        return true;
  }

    /**
     * Return true if all the conditions specified in the restriction are met
     *
     * @param   int        $field_id
     * @param   array<mixed>      $restr     the restriction
     * @param   mixed      $fval       the field value uploaded
     * @return  boolean
     */
    public static function field_matches_restr( $field_id, $restr, $fval )
    {
        $ismatch = true;
        if ( array_key_exists( $field_id, $restr ) ) {
            $rval = $restr[$field_id];
            switch ( $rval ) {
                case '~*~': break;
                case '~-~': if ( $fval ) { $ismatch = false; }
                    break;
                case '~+~': if ( !$fval ) { $ismatch = false; }
                    break;
                default: if ( $rval != $fval )  { $ismatch = false; }
                    break;
            }
        } else {
            error_log( sprintf( __("EWZ: warning - in function field_matches_restr, %d is not a key in the restriction array ", 'entrywizard'), $field_id) );
        }
        return $ismatch;
    }

    /**
     * Validate and sanitize  input that is not string or option
     *
     * There should be no data here - return an error message and set the data to null.
     * If reached, this function should always return an error.
     *
     * @param   mixed  $val  input value
     * @return  boolean
     */
    public static function validate_img_data( $val )
    {

        if ( !( is_string( $val ) &&
                 preg_match( '/^ewz_img/', $val ) ) ) {
            throw new EWZ_Exception( __('"Image" type should be a file upload, not data') );
        }
        return true;
    }

    /**
     * Validate  string input
     *
     * Check length constraints in $fdata. If not satisfied, return a message.
     *
     * @param   array<mixed>   $fdata  input field data
     * @param   mixed   $val    input value
     * @return  boolean
     */
    public static function validate_str_data( $fdata, &$val )
    {
        $maxchars = 5000;   // outer limit, overridden by maxstringchars

        // put some outer length limit on to start with, and  html_entity_decode the string
        if( !self::to_string( $val, array('len' => 9000 ) ) ){
            throw new EWZ_Exception(  __("Invalid format for text input",'entrywizard') );
        }
        $val = str_replace('\\', '', $val );  // messes up stripslashes and serialize. Its hard
                                              // to see a legit use for this, so easier to just remove
        if ( isset( $fdata['maxstringchars'] ) && is_int( $fdata['maxstringchars'] ) ){
            $maxchars = $fdata['maxstringchars'];
        }
        if ( strlen( str_replace("\r", '', $val )) > $maxchars ){  // newlines returned as \n\r, counted as 1 char by html
            $l = strlen( $val ); $m =  mb_strlen( $val );

            $val = substr( $val, 0, $maxchars );
            throw new EWZ_Exception( sprintf(__("Text starting '%s' is too long (%d / %d). Limit is %d (including newlines)",'entrywizard'),
                                             $val, $l, $m, $maxchars  ) );
        }
        return true;
    }

    /**
     * Validate option input
     *
     * Check $val is one of the options in $fdata, and there are not too many items with that value
     * already selected.
     *
     * @param   Ewz_Field  $field
     * @param   mixed      $val         input from a select list
     * @param   int        $optcount    number of previously processed items with this value
     * @return  boolean
     */
    public static function validate_opt_data( $field, $val, $optcount )
    {
        if( !is_string( $val ) ){
            throw new EWZ_Exception( __("Invalid format for option input" , 'entrywizard'));
        }

        // if the item was required, it would be caught earlier, so allow blank here
        if( !$val ){
            return true;
        }

        if ( array_key_exists( $val, $field->Xmaxnums ) && $field->Xmaxnums[$val] ) {
            if ( intval($field->Xmaxnums[$val]) <= $optcount ) {
                throw new EWZ_Exception( sprintf(__("Too many '%s' values for %s", 'entrywizard'), $val , $field->field_header  ) );
            }
        }
        foreach ( $field->fdata['options'] as $option ) {
            if ( $val == $option['value'] ) {
                return true;
            }
        }
        throw new EWZ_Exception( sprintf(__("Invalid value '%s' for field '%s'", 'entrywizard'), $val, $field->field_header ) );
    }

    /**
     * Validate checkbox input
     * Check input is boolean and check the counts
     *
     * @param   Ewz_Field   $field  input field data
     * @param   mixed       $val    input convertible to boolean
     * @param   integer     $count  input count of checks for this checkbox before this item is counted
     * @return  boolean
     */
    public static function validate_chk_data( $field, $val, $count )
    {
        if( !self::to_bool( $val, '' ) ){                  // changes $val to boolean
            throw new EWZ_Exception( __("Invalid value for checkbox input",'entrywizard' ) );
        }
        if ( isset( $field->fdata['chkmax'] ) && $field->fdata['chkmax'] && $val ) {
            if ( intval($field->fdata['chkmax'] ) <= $count ) {
                throw new EWZ_Exception( sprintf(__("Too many items checked for field '%s'" , 'entrywizard'), $field->field_header ) );
            }
        }
        return $val;
    }


    /**
     * Validate radiobutton input
     *
     * @param   mixed   $val    input string convertible to boolean
     * @param   int     $count  count of items with this button checked  before this item is counted
     * @return  boolean
     */
    public static function validate_rad_data(  $val, $count )
    {
        if( !self::to_bool( $val, '' ) ){
            throw new EWZ_Exception( (__("Invalid value for radiobutton input", 'entrywizard') ) );
        }
        if( $val && ( 1 <= $count ) ){  // this was checked and so was a previous item
            throw new EWZ_Exception( __("More than one radiobutton checked: ", 'entrywizard') );
        }
        return $val;
    }
}
