<?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");
require_once( EWZ_PLUGIN_DIR . "classes/ewz-item.php");
require_once( EWZ_PLUGIN_DIR . "classes/ewz-permission.php");
require_once( EWZ_PLUGIN_DIR . "classes/ewz-settings.php");
require_once( EWZ_CUSTOM_DIR . "ewz-custom-data.php");

/* * ***********************************************************
 * Interaction with the EWZ_WEBFORM table
 *
 * *********************************************************** */

class Ewz_Webform extends Ewz_Base {

    const DELETE_ITEMS = 1;
    const FAIL_IF_ITEMS = 0;

    const SPREADSHEET = 0;
    const IMAGES = 1;
    const BOTH = 2;

   /**
    * @var int $webform_id  key
    */
    public $webform_id;
    // database

   /**
    * @var int   $layout_id;
    */
    public $layout_id;

   /**
    * @var int $num_items
    */
    public $num_items;

   /**
    * @var string $webform_title
    */
    public $webform_title;

   /**
    * @var string  $webform_ident
    */
    public $webform_ident;

   /**
    * @var int $webform_order
    */
    public $webform_order;

   /**
    * @var bool $upload_open
    */
    public $upload_open;

   /**
    * @var array<int,int> $open_for
    */
    public $open_for;

    /**
    * @var string $prefix
    */
    public $prefix;

   /**
    * @var bool $apply_prefix
    */
    public $apply_prefix;

   /**
    * @var bool $gen_fname
    */
    public $gen_fname;

   /**
    * @var array<string,mixed> $attach_prefs
    */
    public $attach_prefs;

   /**
    * @var bool $hidden
    */
    public $hidden;

   /**
    * @var array<int,string> $randmap
    */
    private $randmap;       // array of random numbers indexed by item_id, used in prefixes

    // extra

   /**
    * @var bool $can_download
    */
    public $can_download;

   /**
    * @var bool $can_edit_webform
    */
    public $can_edit_webform;

   /**
    * @var bool $can_manage_webform
    */
    public $can_manage_webform;

    /**
    * @var int $itemcount
    */
    public $itemcount;

   /**
    * @var string $open_for_string
    */
    public $open_for_string = '';

    /**
    * @var bool $auto_close
    */
    public $auto_close;

   /**
    * @var string $auto_close_date
    */
    public $auto_close_date;

    /**
    * @var string $auto_close_time
    */
    public $auto_close_time;

   /**
    * @var bool $auto_open
    */
    public $auto_open;
   /**
    * @var string $auto_open_date
    */
    public $auto_open_date;

   /**
    * @var string $auto_open_time
    */
    public $auto_open_time;

    /**
    * @var Ewz_Layout $layout
    */
    public $layout;

   /**
    * @var array<int, mixed>  $user_options used for passing to javascript
    */
    public $user_options;

    /**
    * @var string $layout_name
    */
    public $layout_name;

   /**
    * @var bool $canOverride
    */
    public $canOverride;

   /**
    * @var array<int,string> $field_idents
    */
    public $field_idents;

   /**
    * @var  array<int,string>  $field_types
    */
    public $field_types;

    /**
    * @var array<int,string> $field_names
    */
    public $field_names;

   /**
    * @var  array<int,mixed> $field_options
    */
    public $field_options;
   /**
    * @var string $custom_header
    */
    public $custom_header;

   /**
    * @var array<int,string> $custom_options
    */
    public $custom_options;

   /**
    * @var string $layouts_options
    */
    public $layouts_options;

   /**
    * @var string $time_opts_open
    */
    public $time_opts_open;

   /**
    * @var string $time_opts_close
    */
    public $time_opts_close;


   /**
    * @var array<string,bool> $files_done
    */
    private $files_done;     // files added to zipfile for download

    // keep list of db data names/types as a convenience for iteration and so we can easily add new ones.
    // Dont include webform_id

   /**
    * @var array<string, mixed> $varlist
    */
    public static $varlist = array(
        'layout_id' => 'integer',
        'num_items'  => 'integer',
        'webform_title' => 'string',
        'webform_ident' => 'string',
        'webform_order' => 'integer',
        'hidden' => 'boolean',
        'upload_open' => 'boolean',
        'open_for' => 'array',
        'prefix' => 'string',
        'apply_prefix' => 'boolean',
        'gen_fname' => 'boolean',
        'attach_prefs' => 'string',
    );

    /**
     * Return the number of  webforms using the input layout
     *
     * @param   int  $layout_id   id of the layout
     * @return  int  number of webforms
     */
    public static function get_count_for_layout( $layout_id ) {
        global $wpdb;

        $dbwforms_for_layout = wp_cache_get( $layout_id, 'wforms_for_layout' );
        if( !$dbwforms_for_layout ){

            $dbwforms_for_layout = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_WEBFORM_TABLE .
            " WHERE layout_id = %d", $layout_id ) );
            wp_cache_set( $layout_id, $dbwforms_for_layout, 'wforms_for_layout' );
        }
        return $dbwforms_for_layout;
    }

    /**
     * Return an array of all webforms using the input layout
     *
     * @param   int  $layout_id   id of the layout
     * @return  array<int, Ewz_Webform> of all Ewz_Webforms using $layout_id
     */
    public static function get_webforms_for_layout( $layout_id ) {
        global $wpdb;

        $list = $wpdb->get_col( $wpdb->prepare( "SELECT webform_id  FROM " . EWZ_WEBFORM_TABLE .
        " WHERE layout_id = %d  ORDER BY webform_order", $layout_id ) );
        $webforms = array( );
        foreach ( $list as $webform_id ) {
            $webform = new Ewz_Webform( $webform_id );
            array_push( $webforms, $webform );
        }
        return $webforms;
    }

    /**
     * Return an array of layout titles and  "open" status for all webforms using the input
     *
     * @param   string  $filter  optional Ewz_Permission function used to filter the titles
     * @return  array<int, array<string, mixed>> of all webform titles using $layout_id
     */
    public static function get_webform_titles( $filter = 'truefunc' ) {
         global $wpdb;

        $list = $wpdb->get_results( "SELECT webform_id, layout_id, webform_title, upload_open, open_for  FROM " . EWZ_WEBFORM_TABLE .  // no tainted data
        " ORDER BY webform_order"  );
        $titles = array();
        foreach ( $list as $wf ) {
            if( call_user_func( array( 'Ewz_Permission',  $filter ), $wf->webform_id, $wf->layout_id ) ) {
                $open = self::check_unserial_arr( $wf->open_for );
                array_push( $titles, array( 'webform_id' => $wf->webform_id,
                'layout_id' =>  $wf->layout_id,
                'webform_title' => $wf->webform_title,
                'open' => (int)$wf->upload_open,
                'open_for' => $open,
                ) );
            }
        }
        return $titles;
    }


    /**
     * Return an array of all webforms
     *
     * @param   string  $filter  optional  Ewz_Permission  function used to filter the titles
     * @param   boolean $include_hidden   optional function used to filter the titles
     * @return  array<Ewz_Webform> of all  webforms
     */
    public static function get_all_webforms( $filter = 'truefunc', $include_hidden = true ) {
        global $wpdb;
        $inc = '';
        if( !$include_hidden ){
            $inc = ' WHERE hidden = 0 ';
        }
        $list = $wpdb->get_col( "SELECT webform_id  FROM " . EWZ_WEBFORM_TABLE . " $inc ORDER BY webform_order" );  // no tainted data
        $webforms = array( );
        foreach ( $list as $webform_id ) {
            if ( call_user_func( array( 'Ewz_Permission',  $filter ), $webform_id ) ) {
                $webform = new Ewz_Webform( $webform_id );
                array_push( $webforms, $webform );
            }
        }
        return $webforms;
    }

    /**
     * @return  array<string> of all  webform idents
     */
    public static function get_all_idents() {
        global $wpdb;
        $list = $wpdb->get_col( "SELECT webform_ident  FROM " . EWZ_WEBFORM_TABLE . " ORDER BY webform_order" );   // no tainted data
        return $list;
    }


    /**
     * Get a count of the webforms
     *
     * @return   int count
     */
    public static function count_visible_webforms( ) {
        global $wpdb;
        $count = $wpdb->get_var(  "SELECT count(*) FROM " . EWZ_WEBFORM_TABLE . " WHERE hidden = 0" );       // no tainted data
        return $count;
    }

    /**
     * Get the list of  webform_ids sorted by webform_order, and then adjusted for any gaps or duplications
     *
     * @param    boolean $only_visible  -- if true, only return webforms visible to current user
     * @return   array<int,int>
     */
    public static function get_webform_order( $only_visible = false ) {
        global $wpdb;

        $vis = $only_visible ? ' WHERE hidden = 0 ' : '';
        $list = $wpdb->get_results(  "SELECT webform_id FROM " . EWZ_WEBFORM_TABLE . " $vis  ORDER BY webform_order"  );       // no tainted data
        $order = array();
        $n=0;
        // doing it this way instead of using just the saved webform_order fixes any duplication or gaps in positions due to hidden webforms
        // not having their order set correctly, or people changing order on some without having permission to edit all webforms.
        foreach( $list as $webform ){
            $order[$webform->webform_id] = $n++;
        }
        return $order;
    }

    /**
     * Make sure the input webform_id is a valid one
     *
     * @param    int     $webform_id
     * @return   boolean true if $webform_id is the key for a EWZ_WEBFORM_TABLE row, otherwise false
     */
    public static function is_valid_webform( $webform_id ) {
        global $wpdb;

        $wcount = $wpdb->get_var( $wpdb->prepare( "SELECT count(*) FROM " . EWZ_WEBFORM_TABLE . " WHERE  webform_id = %d", $webform_id ) );
        return ( 1 == $wcount );
    }


    /**
     * Deal with upgrade -- set num_items field in webforms
     * @return void
     */
    public static function upgrade_set_num_items(){
        global $wpdb;
        // cache_no_change~wforms_for_layout~
        // cant use built-in update function here
        $wpdb->query( "UPDATE " . EWZ_WEBFORM_TABLE . " wf " .                                         // upgrade no tainted data
        "   SET num_items = ( SELECT  max_num_items from " .  EWZ_LAYOUT_TABLE . " lay " .
        " WHERE lay.layout_id = wf.layout_id ) " );
        if( $wpdb->last_error ){
            throw new EWZ_Exception( 'Upgrade of webform table failed', $wpdb->last_error );
        }
    }
    /**
     * Deal with upgrade -- set webform_order field in webforms
     * @return void
     */
    public static function upgrade_set_webform_order(){
        global $wpdb;
        $list = $wpdb->get_col( "SELECT webform_id  FROM " . EWZ_WEBFORM_TABLE . " ORDER BY webform_id" );   // no tainted data
        // cache_no_change~wforms_for_layout~
        $n = 0;
        foreach ( $list as $webform_id ) {
            $wpdb->query( $wpdb->prepare( "UPDATE " . EWZ_WEBFORM_TABLE .                                    // upgrade
            "   SET webform_order = %d WHERE webform_id = %d ", $n, $webform_id ) );
            if( $wpdb->last_error ){
                throw new EWZ_Exception( 'Upgrade of webform order failed', $wpdb->last_error );
            }
            ++$n;
        }
    }


    /**
     * Return the num_items field for the input webform -- number of items allowed
     * @param int $webform_id
     * @return int $n number of items allowed to be uploaded
     */
    public static function get_num_items( $webform_id ){
        global $wpdb;

        $n = $wpdb->get_var( $wpdb->prepare( "SELECT num_items  FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_id = %d",  $webform_id ) );
        return $n;
    }

    /**
     * Return the layout_id for a webform_id.
     * @param int $webform_id
     *
     * @return int
     **/
    public static function get_layout_id( $webform_id )
    {
        global $wpdb;
        $layout_id = $wpdb->get_var( $wpdb->prepare( "SELECT layout_id  FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_id = %d",  $webform_id ) );
        return $layout_id;
    }

    /*     * ****************** Construction ************************* */

    /**
     * Assign the object variables from an array
     *
     * Calls parent::base_set_data with the list of variables and the data structure
     *
     * @param  array<string,mixed>  $data input data.
     * @return void
     */
    public function set_data( $data ) {
        parent::base_set_data( array_merge( self::$varlist, array( 'webform_id' => 'integer' ) ), $data );
        $this->can_download = Ewz_Permission::can_download( $data['webform_id'], $data['layout_id'] );
        $this->can_edit_webform = Ewz_Permission::can_edit_webform( $data['webform_id'] );
        $this->can_manage_webform = Ewz_Permission::can_manage_webform( $data['webform_id'], $data['layout_id']  );
    }

    /**
     * Constructor
     *
     * // TODO  clean this up -- make sure all variables get set or document what is not needed
     *                        -- eliminate duplicated code
     * @param  mixed      $init  webform_id or array of data
     * @param  Ewz_Layout $layout
     * @return void
     */
    public function __construct( $init, $layout = null ) {
        if( $layout ){
            $this->construct_with_layout( $init, $layout );
        } else {
            if ( is_numeric( $init ) ){
                if( Ewz_Base::is_pos_int( $init ) ) {
                    $this->create_from_id( $init );
                } else {
                    $this->create_from_id( intval( $init, 10 ) );
                }
            } elseif ( is_string( $init ) ) {
                $this->create_from_ident( $init );
            } elseif ( is_array( $init ) ) {
                if( isset( $init['webform_id'] ) &&  ( $init['webform_id'] > 0 ) ){
                    $this->update_from_data( $init );
                } else {
                    $init['webform_id'] = 0;
                    $this->create_from_data( $init );
                }
            } else {
                throw new EWZ_Exception( 'Invalid webform constructor' );
            }
            if ( $this->webform_id ) {
                $this->itemcount = Ewz_Item::get_itemcount_for_webform( $this->webform_id );
            } else {
                // no webform_id means creating a new one
                if ( !Ewz_Permission::can_manage_all_webforms() ) {
                    throw new EWZ_Exception( "No permission" );
                }
                $this->itemcount = 0;
            }
        }

        // add variables required for javascript
        if ( $this->open_for ) {
            $this->open_for_string = '<i>Currently open only for the following users:</i> ';
            foreach( $this->open_for as $user_id ){
                $udata = get_userdata($user_id);
                $this->open_for_string .= "<br>" . $udata->last_name . ', ' . $udata->first_name . ' ( ' . $udata->user_login . ', ' . $udata->user_email . ' )';
            }
        }
    }

    /**
     * Constructor called from rating
     *
     * @param  mixed      $id    webform_id or array of data
     * @param  Ewz_Layout $layout
     * @return void
     */
    public function construct_with_layout( $id, $layout ) {
        $this->create_from_id_layout( $id, $layout );
        $this->itemcount = Ewz_Item::get_itemcount_for_webform( $this->webform_id );
    }

    /**
     * Create a new webform object from the webform_id and layout by getting the data from the database
     *
     * @param  int  $id the webform id
     * @param  Ewz_Layout  $layout the layout
     * @return void
     */
    protected function create_from_id_layout( $id, $layout ) {
        global $wpdb;
        $dbwebform = $wpdb->get_row( $wpdb->prepare( "SELECT webform_id, " . implode( ',', array_keys( self::$varlist ) ) .
        " FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_id=%d", $id ), ARRAY_A );
        if ( !$dbwebform ) {
            throw new EWZ_Exception( 'Unable to find matching webform for id', $id );
        }
        if( $dbwebform['layout_id'] != $layout->layout_id ){
            throw new EWZ_Exception( 'Attempt to create webform with invalid layout', $id );
        }
        $this->layout = $layout;
        $this->can_download = Ewz_Permission::can_download( $id, $dbwebform['layout_id'] );
        $this->can_edit_webform = Ewz_Permission::can_edit_webform( $id );
        $this->can_manage_webform = Ewz_Permission::can_manage_webform( $id, $dbwebform['layout_id'] );
        if( !$this->layout->override ||  !$dbwebform['num_items'] ){
            $dbwebform['num_items'] = $this->layout->max_num_items;
        }
        $this->set_data( $dbwebform );
        $this->set_auto_times();
    }

    /**
     * Create a new webform object from the webform_id by getting the data from the database
     *
     * @param  int  $id the webform id
     * @return void
     */
    protected function create_from_id( $id ) {
        global $wpdb;
        $dbwebform = $wpdb->get_row( $wpdb->prepare( "SELECT webform_id, " . implode( ',', array_keys( self::$varlist ) ) .
        " FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_id=%d", $id ), ARRAY_A );
        if ( !$dbwebform ) {
            throw new EWZ_Exception( 'Unable to find matching webform for id', $id );
        }
        $this->layout = new Ewz_Layout( $dbwebform['layout_id'] );
        // If the layout does not allow num_items overrides, or if the webform did not set any override, set num_items to the layout value
        if( !$this->layout->override ||  !$dbwebform['num_items'] ){
            $dbwebform['num_items'] = $this->layout->max_num_items;
        }
        $this->set_data( $dbwebform );
        $this->set_auto_times();
    }

    /**
     * Create a new webform object from the webform ident by getting the data from the database
     *
     * @param  string   $ident      the webform ident
     * @return void
     */
    protected function create_from_ident( $ident ) {
        global $wpdb;
        $dbwebform = $wpdb->get_row( $wpdb->prepare( "SELECT webform_id, " . implode( ',', array_keys( self::$varlist ) ) .
        " FROM " .  EWZ_WEBFORM_TABLE .
        " WHERE webform_ident=%s", $ident ), ARRAY_A );
        if ( !$dbwebform ) {
            throw new EWZ_Exception( 'Unable to find matching webform for identifier', $ident );
        }
        $this->can_download = Ewz_Permission::can_download( $dbwebform['webform_id'], $dbwebform['layout_id']);
        $this->can_edit_webform = Ewz_Permission::can_edit_webform( $dbwebform['webform_id'] );
        $this->can_manage_webform = Ewz_Permission::can_manage_webform( $dbwebform['webform_id'], $dbwebform['layout_id']  );

        $this->layout = new Ewz_Layout( $dbwebform['layout_id'] );
        if( !$this->layout->override || !isset( $dbwebform['num_items'] ) || !$dbwebform['num_items'] ){
            $dbwebform['num_items'] = $this->layout->max_num_items;
        }
        $this->set_data( $dbwebform );
        $this->set_auto_times();
    }

    /**
     * Create a webform object from $data, which does not contain a webform_id
     * @param  array<string,mixed>  $data input data.
     * @return void
     */
    protected function create_from_data( $data ) {

        if ( array_key_exists( 'upload_open', $data ) && $data['upload_open'] ) {
            $data['open_for'] = array( );
        } else {
            $data['open_for'] = array_map( function($v){ return (int)$v; },
            array_filter( isset( $data['o_user'] ) ?
            $data['o_user'] : array( ), function($v){ return ($v != ""); } ) );
        }
        if ( array_key_exists( 'webform_id', $data ) && ( $data['webform_id'] > 0 ) ) {
            throw new EWZ_Exception("create_from_data called with id");
        }
        if ( !isset( $data['layout_id'] ) ) {
            $data['layout_id'] = 0;
        } else {
            $data['layout_id'] = intval( $data['layout_id'], 10 );
        }
        $this->layout = new Ewz_Layout( $data['layout_id'] );
        if( !$this->layout->override || !isset( $data['num_items'] ) || !$data['num_items'] ){
            $data['num_items'] = $this->layout->max_num_items;
        }
        $this->set_data( $data );
        $this->check_errors();
        if(isset($data['auto_close']) && $data['auto_close'] ){
            $this->schedule_action( 'close', $data['auto_close_date'] . ' '.  $data['auto_close_time'] );
        } else {
            $this->unschedule_action('close');
        }
        if( isset($data['auto_open']) && $data['auto_open'] ){
            $this->schedule_action( 'open', $data['auto_open_date'] . ' '.  $data['auto_open_time'] );
        } else {
            $this->unschedule_action('open');
        }

        $this->set_auto_times();
    }
    /**
     * Create a webform object from $data, which contains a webform_id
     *
     * @param  array<string,mixed>  $data input data.
     * @return void
     */
    protected function update_from_data( $data ) {
        global $wpdb;
        if ( array_key_exists( 'upload_open', $data ) && $data['upload_open'] ) {
            $data['open_for'] = array( );
        } else {
            $data['open_for'] = array_map( function($v){ return (int)$v; },
            array_filter( isset( $data['o_user'] ) ?
            $data['o_user'] : array( ), function($v){ return ($v != ""); } ) );
        }
        if ( !isset( $data['webform_id'] ) ) {
            throw new EWZ_Exception("update_from_data called with no webform_id");
        }
        $data['webform_id'] = intval( $data['webform_id'], 10 );

        if ( isset( $data['layout_id'] ) ) {
            $data['layout_id'] = intval( $data['layout_id'], 10 );
        }
        $this->layout = new Ewz_Layout( $data['layout_id'] );
        if( !$this->layout->override || !isset( $data['num_items'] ) || !$data['num_items'] ){
            $data['num_items'] = $this->layout->max_num_items;
        }

        $dbwebform_arr = $wpdb->get_row( $wpdb->prepare( "SELECT " . implode( ',', array_keys( self::$varlist ) ) .
        " FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_id=%d", $data['webform_id'] ), ARRAY_A );
        if ( !$dbwebform_arr ) {
            throw new EWZ_Exception( 'Unable to find matching webform for id', $data['webform_id'] );
        }

        foreach( $dbwebform_arr as $key => $val ){
            if( !isset($data[$key]) ){
                $data[$key] = $val;
            }
        }

        $this->set_data( $data );
        $this->check_errors();
        if( $data['auto_close'] ){
            $this->schedule_action( 'close',  $data['auto_close_date'] . ' '.  $data['auto_close_time'] );
        } else {
            $this->unschedule_action('close');
        }
        if( isset($data['auto_open']) && $data['auto_open'] ){
            $this->schedule_action( 'open', $data['auto_open_date'] . ' '.  $data['auto_open_time'] );
        } else {
            $this->unschedule_action('open');
        }

        $this->set_auto_times();
    }


    /*     * ******************  Download Functions ********************* */

    /**
     * Generate the zip archive file of images using PHPs ZipArchive
     * Called if there is no access to a "zip" command on the server
     *
     * Use a random filename because the file will be temporarily stored in a web-accessible folder
     *
     * @param   array<int,Ewz_Item>   $items
     * @param   int     $inclusion  if self::BOTH, add the spreadsheet to the archive
     * @return  string   the option name that contains the filename
     */
    public function generate_zip_archive( $items, $inclusion ) {
        if ( !$this->can_download ) {
            throw new EWZ_Exception( "No Permission" );
        }
        if ( count( $items ) < 1 ) {
            throw new EWZ_Exception( "No matching items found." );
        }
        $date = current_time( 'Ymd_Hi' );                // for filename ewz_{$date}_{$rand}.zip of archive file
        $up = $this->ewz_upload_dir( wp_upload_dir() );  // uploads/ewz_img_uploads/$this->webform_ident
        if ( !is_dir( $up['path'] ) ) {
            throw new EWZ_Exception( "Images not found." );
        }

        $rand = $this->randstring(15);
        $key  = $this->randstring(5);
        $archive_fname = "ewz_{$date}_{$rand}.zip";
        $archive_path = $up['path'] . "/$archive_fname";
        $archive_url = $up['url'] . "/$archive_fname";

        update_option( "ewz_{$this->webform_id}_{$key}",  $archive_url );

        // remove any old zip files from this webform
        $zipfiles =  glob( $up['path'] . "/ewz_*.zip" );
        $now = time();
        foreach( $zipfiles as $file ){
            if( $now - filemtime( $file ) > 1200 ){    // 20 minutes -- in case there is a need to download via ftp after file is generated
                unlink( $file );
            }
        }

        // create a new archive and return the option name that contains the filename
        $this->zip_items( $items, $archive_path, $inclusion );
        return "ewz_{$this->webform_id}_{$key}";
    }

    /**
     * Use the server "zip" command to generate the zip archive of images,
     * ( optionally including the csv file ) and print it to stdout.
     *
     * This avoids the overhead of generating a file, and the need for a
     * second call to read it. The output is slowed down to avoid large IO spikes
     * that may trigger blocking by the webhost.
     * If the csv file was included, delete it when done.
     *
     * @param   array<int,Ewz_Item>   $items
     * @param   int     $inclusion  if self::BOTH, add the spreadsheet to the archive
     * @return  void
     */
    public function gen_and_echo_archive( $items, $inclusion ){

        if ( !$this->can_download ) {
            throw new EWZ_Exception( "No Permission" );
        }
        if ( !$items ) {
            throw new EWZ_Exception( "No Items Found" );
        }
        $bufsize = 8192;  // seems to be the standard default

        $date = current_time( 'Ymd_Hi' );  // for filename ewz_{$date}.zip of downloaded zip attachment
        $fnames = $this->get_fname_list( $items,  $inclusion );

        self::ewz_disable_gzip();

        // options for linux "zip" command:
        // -0 = no compression
        // -j = strip path, use only filename
        // -q = no extra output info
        $fp = popen("zip -0 -j -q - $fnames", 'r');        // see testing notes
        if( !$fp ){                                        // see testing notes
            error_log("EWZ: Zip command failed");
            throw new EWZ_Exception("Zip command failed");
        }

        header( 'Content-Type: application/octet-stream');
        header( "Content-disposition: attachment; filename=ewz_{$date}.zip; ");

        while( !feof($fp) ) {
            echo fread($fp, $bufsize);
            flush();
            usleep(100);  // needed to avoid IO spikes
        }
        pclose($fp);
        if ( self::BOTH == $inclusion ){
            $matches = array();
            preg_match( '/ ([^ ]+) *$/', $fnames, $matches );
            if( isset($matches[1]) && is_file( $matches[1]  ) ) {
                unlink( $matches[1] );
            }
        }
        exit;
    }

    /**
     * Echo to stdout the file defined by $archive_key, then delete it
     *
     * @param  string    $archive_key
     * @return void
     */
    public function echo_stored_archive( $archive_key ){

        if ( !$this->can_download ) {
            throw new EWZ_Exception( "No Permission" );
        }
        $archive_url =  get_option( $archive_key, admin_url( 'admin.php?page=entrywizard' ) );
        delete_option( $archive_key );

        self::ewz_disable_gzip();

        // display it, making sure the redirect location is not cached
        header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
        header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
        header( "Cache-Control: no-store, no-cache, must-revalidate" );
        header( "Cache-Control: post-check=0, pre-check=0", false );
        header( "Pragma: no-cache" );
        header( "Location: $archive_url" );
        exit;
    }



    /**
     * Return the list of filenames ( complete paths ) defined by $items, including the
     * csv file if $inclusion == self::BOTH, to be included in the zip archive
     * No file renaming here - just return whatever is defined in the item
     *
     * @param   array<int,Ewz_Item>   $items
     * @param   int    $inclusion     self::BOTH or self::IMAGES
     * @return  string    space-separated list of filenames to be included in the zip archive
     */
    protected function get_fname_list( $items,  $inclusion )
    {

        if ( !$this->can_download ) {
            throw new EWZ_Exception( "No Permission" );
        }
        $msg = '';
        $this->files_done = array();

        $fnames = '';
        foreach ( $items as $item ) {
            foreach ( $item->item_files as $item_file ) {
                if ( !isset( $item_file['fname'] ) ) {
                    continue;   // ignore items with no image file
                }
                if ( is_file( $item_file['fname'] ) ) {
                    $fnames .= $item_file['fname'] . " ";
                    $this->files_done[$item_file['fname']] = true;
                } else {
                    error_log("EWZ: cant find " .  $item_file['fname'] );
                    $msg .= "\n\nUnable to find file " . basename( $item_file['fname'] );
                }
            }
        }
        $csv_fname = '';
        if ( self::BOTH == $inclusion  ) {
            $csv_fname = $this->download_spreadsheet( $items, $inclusion );
            if ( is_file( $csv_fname ) ) {
                $fnames .= $csv_fname;
            } else {
                $msg .= "\n\nUnable to find .csv file ";
            }
        }
        if ( $msg ) {
            error_log("EWZ: error generating file list: $msg");
        }
        return $fnames;
    }


    /**
     * Create the zip archive of images ( and possibly spreadsheet ) using PHPs ZipArchive
     * If there is a prefix set but not yet applied, apply it to the
     * filename in the archive
     *
     * @param   array<int,Ewz_Item>   $items
     * @param   string              $fpath        full path name of archive to be created
     * @param   int                 $inclusion       if self::BOTH, add the spreadsheet to the archive
     * @return  void
     */
    protected function zip_items( $items, $fpath, $inclusion ) {

        if ( !$this->can_download ) {
            throw new EWZ_Exception( "No Permission" );
        }
        set_time_limit( EWZ_FILE_DOWNLOAD_TIME );
        $tmpn = 0;

        $zip = new ZipArchive();
        $msg = '';
        $this->files_done = array();

        $fields = Ewz_Field::get_fields_for_layout( $this->layout_id, 'ss_column' );

        $result = $zip->open( $fpath, ZipArchive::CREATE|ZipArchive::OVERWRITE );
        if ( $result === TRUE ) {
            foreach ( $items as $item ) {
                foreach ( $item->item_files as $item_file ) {
                    if ( !isset( $item_file['fname'] ) ) {
                        continue;   // ignore items with no image file
                    }

                    ++$tmpn;
                    // a very rough-and-ready way to allow more time if there are a lot of image files
                    // adds EWZ_FILE_DOWNLOAD_TIME seconds to the time limit for every 25 files.
                    if ( $tmpn > 25 ) {
                        set_time_limit( EWZ_FILE_DOWNLOAD_TIME );
                        $tmpn = 0;
                    }
                    $newfilename = '';
                    if ( is_file( $item_file['fname'] ) ) {
                        if( $this->apply_prefix ){
                            // if the prefix/rename was already done, just add the file with it's current name
                            $newfilename = basename( $item_file['fname'] );

                        } else {
                            // if a prefix/rename has been set but not yet done, add it now
                            $newfilename = $this->get_new_filename( $fields, $item, $item_file );
                        }
                        $zip->addFile( $item_file['fname'],  $newfilename );
                        $this->files_done[$newfilename] = true;

                    } else {
                        error_log("EWZ: cant find " .  $item_file['fname'] );
                        $msg .= "\n\nUnable to find file " . basename( $item_file['fname'] );
                    }
                }
            }

            $csv_fname = '';
            if ( self::BOTH == $inclusion  ) {
                $csv_fname = $this->download_spreadsheet( $items, $inclusion );
                if ( is_file( $csv_fname ) ) {
                    $zip->addFile( $csv_fname, basename( $csv_fname ) );
                } else {
                    $msg .= "\n\nUnable to find .csv file ";
                }
            }
            if ( $msg ) {
                error_log("EWZ: zip error: $msg");
                $zip->addFromString( 'ERRORS.txt',
                "\nThe following errors were encountered while generating the zip archive: $msg" );
            }
            if( $zip->close() === FALSE ){
                throw new EWZ_Exception( "Failed to close zip file. " . $msg );
            }
            if ( ( self::BOTH == $inclusion ) && is_file( $csv_fname ) ) {
                unlink( $csv_fname );
            }
            chmod( $fpath, 0644);
        } else {
            throw new EWZ_Exception( "Sorry, there was a problem creating the zip archive.  If this continues, please contact your administrator.", $result );
        }
    }

    /**
     * Return a filename generated from the prefix expression
     * Only called for filenames generated for download and not stored on server
     *
     * @param  array<Ewz_Field>      $fields   the fields of the layout
     * @param  Ewz_Item   $item       the item containing the file
     * @param  array<string,mixed>   $item_file  of file data  a single component of the $item->item_files array
     * @return string     the generated filename (including extension)
     */
    public function get_new_filename( $fields, $item, $item_file ){

        if( $this->prefix ){
            $custom = new Ewz_Custom_Data( $item->user_id );
            $subst_data = array(
                'file_field_id' => $item_file['field_id'],
                'user_id' => $item->user_id,
                'item_id' => $item->item_id,
            );
            /** @phpstan-ignore-next-line */
            foreach ( $custom as $custkey => $custval ) {
                $subst_data[$custkey] = $custval;
            }
            foreach ( $fields as $fid=>$field ){
                if( isset( $item->item_data[$fid] ) ){
                    $subst_data[$fid] = $item->item_data[$fid];
                } else {
                    $subst_data[$fid] = '';
                }
            }
            if( $this->gen_fname ){
                $ext = pathinfo( $item_file['fname'], PATHINFO_EXTENSION );
                $prefix1 = $this->generated_prefix( $subst_data, true );   // allow all fields, even text, because not saving on server
                if ( strpos( $prefix1, '[~1]' ) !== false ) {
                    // make sure the filename is unique -- replace '[~1]' by 1,2,3,... until it is
                    $uniq = $this->get_fname_num( $prefix1, $ext );
                    $prefix1 = str_replace( '[~1]', $uniq, $prefix1 );
                }
                return sanitize_file_name( $prefix1 . '.' . $ext );
            } else {
                // allow all fields, even text, since not saving file on server
                return sanitize_file_name( $this->generated_prefix( $subst_data, true ) . basename( $item_file['fname'] ) );
            }
        } else {
            return  basename( $item_file['fname'] );
        }
    }

    /**
     * Return a number to replace '[~1]' to make the filename unique in it's download batch.
     * Substitutes 1,2,3,.... in turn for '[~1]' in $in_fname until
     *              $this->files_done["$in_fname.$ext"] is not defined.
     * @param  string  $in_fname  an input filename that should contain the string  '[~1]'
     * @param  string  $ext       a filename extension
     * @return string   an integer between 1 and 999 as a string
     */
    public function get_fname_num( $in_fname, $ext ){
        if( strlen( $in_fname ) < 5 ){
            return '';
        }
        if( strpos( $in_fname, '[~1]' ) === false ){
            return '';
        }
        $num = 1;
        while( $num < 999 ){
            $testfname = str_replace( '[~1]', "$num", $in_fname );
            if( !isset( $this->files_done["{$testfname}.{$ext}"] ) ){
                return "$num";
            }
            ++$num;
        }
        return "$num";
    }

    /**
     * Generate the requested spreadsheet and print to stdout or save as a file
     *
     * Generates a .csv-formatted summary of the uploaded items for the webform
     * Prints to stdout if $include == self::SPREADSHEET, otherwise saves to a file
     *    with a randomized filename for later downloading
     *
     * @param  array<Ewz_Item> $items   -- items for downloading
     * @param  int   $include  self::SPREADSHEET if just downloading spreadsheet,
     *                                       self::BOTH if downloading images and spreadsheet
     * @param  string   $aflag    'C' if this is an automated (cron) job, otherwise ''
     * @return string   filename of saved file, no return if data sent to stdout
     */
    public function download_spreadsheet( $items, $include, $aflag = '' ) {

        $is_auto = defined( 'DOING_CRON' ) && DOING_CRON && ( $include ===  self::BOTH ) && ( $aflag === 'C' );
        if ( !$this->can_download  &&  !$is_auto ) {
            throw new EWZ_Exception( "No Permission" );
        }
        if ( count( $items ) < 1 ) {
            throw new EWZ_Exception( "No matching items found." );
        }

        $fields = Ewz_Field::get_fields_for_layout( $this->layout_id, 'ss_column' );
        $extra_cols = Ewz_Layout::get_extra_cols( $this->layout_id );
        $rows = $this->get_rows_for_ss( $fields, $extra_cols, $items, $is_auto );
        // sort each row by it's key, which is the ss column
        foreach ( array_keys($rows) as $k ) {
            ksort( $rows[$k]);
        }
        $out = '';
        $outpath = '';
        if( $include ==  self::BOTH ){
            $uploadstruct = wp_upload_dir();
            $up = $this->ewz_upload_dir( $uploadstruct );         // uploads/ewz_img_uploads/$this->webform_ident
            // If it doesnt exist, create the upload dir and give it the required permissions
            if ( !is_dir( $up['path'] ) && !$is_auto ) {
                mkdir( $up['path'] );
                chmod($up['path'], 0755);
                $outp = fopen( $up['path'] . "/index.php", 'w' );
                fwrite( $outp, "<?php\n   //No listing\n?>" );
                fclose( $outp );
                chmod($up['path'] . "/index.php", 0644);
            }
            $date = current_time( 'Ymd_Hi' );      // for filename ewz_{$date}_{$rand}.csv of spreadsheet within archive
            $rand =  $this->randstring(5);
            $outpath = $up['path'] . "/ewz_{$date}_{$rand}.csv";
            $out = fopen( $outpath, 'w' );
            if( !$out ){
                throw new EWZ_Exception( "Failed to open csv file for writing");
            }
        } else {
            $now = current_time( 'Ymd_Hi' );   // for filename ewzdata_{$now}.csv of downloaded .csv attachment
            $filename = "ewzdata_{$now}.csv";
            header( "Content-Disposition: attachment; filename=\"$filename\"" );
            header( "Content-Type: text/csv" );
            header( "Cache-Control: no-cache" );

            $out = fopen( "php://output", 'w' );  // write directly to php output, not to a file
            if( !$out ){
                throw new EWZ_Exception( "Failed to open php output for writing");
            }
        }
        foreach ( $rows as $r ) {
            fputcsv( $out, $r, "," );
        }
        fclose( $out );

        if( $include == self::BOTH) {
            chmod( $outpath, 0644);
            return $outpath;
        } else {
            // this forces the download dialog
            exit();
        }
    }


    /**
     * Return the header row for the spreadsheet
     *
     * @param   array<Ewz_Field> $fields  fields array from layout - data input via the webform
     * @param   array<string,int>  $extra_cols of extra columns array from layout - other data for display
     * @return  array<int,string> of headers
     */
    protected function get_headers_for_ss( $fields, $extra_cols ) {
        $img_data = array( 'ss_col_w' => 'Width', 'ss_col_h' => 'Height', 'ss_col_o' => 'Orientation' );
        $hrow = array( );
        foreach ( $fields as $field ) {
            if ( $field->ss_column >= 0 ) {
                $hrow[$field->ss_column] = $field->field_ident;
            }
            if ( 'img' == $field->field_type ) {
                foreach ( $img_data as $ss_img_col => $ss_img_header ) {
                    $ssimgcol = $field->fdata[$ss_img_col];
                    if ( $ssimgcol >= 0 ) {
                        $hrow[$ssimgcol] = $ss_img_header;
                    }
                }
            }
            if ( 'str' == $field->field_type ) {
                $sstxtcol = isset( $field->fdata['ss_col_fmt'] ) ? $field->fdata['ss_col_fmt'] : -1;
                if ( $sstxtcol >= 0 ) {
                    $hrow[$sstxtcol] = 'Formatted ' . $field->field_ident;
                }
            }
        }
        $dheads = Ewz_Layout::get_all_display_headers();
        foreach ( $extra_cols as $xcol => $sscol ) {
            if ( $sscol >= 0 && isset( $dheads[$xcol] ) ) {
                $hrow[$sscol] = $dheads[$xcol]['header'];
            }
        }
        $maxk = max( array_keys( $hrow ) );
        for ( $i = 0; $i <= $maxk; ++$i ) {
            if ( !isset( $hrow[$i] ) ) {
                $hrow[$i] = "Blank " . chr(65 + $i);
            }
        }
        return $hrow;
    }

    /**
     * Return the body of the spreadsheet
     *
     * @param   array<Ewz_Field> $fields  fields array from layout - data input via the webform
     * @param   array<string,int> $extra_cols of extra columns array from layout - other data for display
     * @param   array<Ewz_Item> $items      of data uploaded via webform
     * @param   bool  $is_auto
     * @return  array<int,array<int,string>> to be displayed in spreadsheet
     *
     */
    protected function get_rows_for_ss( $fields, $extra_cols, $items, $is_auto=false ) {

        $rows = array( );
        $rows[0] = $this->get_headers_for_ss( $fields, $extra_cols );
        $maxcol = max( array_keys( $rows[0] ) );
        $n = 1;

        $this->files_done = array();
        foreach ( $items as $item ) {

            $itemdata = $this->get_item_data_for_ss( $fields, $item, $maxcol );

            $filedata = $this->get_file_data_for_ss( $fields, $item, $maxcol );

            $customdata = $this->get_custom_data_for_ss( $item, $extra_cols, $maxcol, $is_auto );

            $labeldata = $this->get_label_data_for_ss( $fields, $item );



            for ( $col = 0; $col <= $maxcol; ++$col ) {
                if ( $itemdata[$col] ) {
                    $rows[$n][$col] = $itemdata[$col];
                } elseif ( isset($filedata[$col] ) && $filedata[$col] ) {
                    $rows[$n][$col] = $filedata[$col];
                } elseif (isset($customdata[$col]) && $customdata[$col] ) {
                    $rows[$n][$col] = $customdata[$col];
                } elseif ( isset($labeldata[$col]) && $labeldata[$col] ) {
                    $rows[$n][$col] = $labeldata[$col];
                } else {
                    $rows[$n][$col] = '';
                }
            }
            ++$n;
        }

        return $rows;
    }


    /**
     * Return the custom data to be displayed in the spreadsheet for an item
     *
     * @param   Ewz_Item   $item         item the data is to be attached to
     * @param   array<string,int>     $extra_cols of  extra columns array from layout
     * @param   int        $maxcol       max column to be shown
     * @param   bool       $is_auto
     * @return  array<string>      the spreadsheet data for the custom fields
     */
    private function get_custom_data_for_ss( $item, $extra_cols, $maxcol, $is_auto=false ) {

        $customrow = array_fill( 0, $maxcol + 1, '' );
        $user = get_userdata( $item->user_id );
        $display = Ewz_Layout::get_all_display_data();
        $custom = new Ewz_Custom_Data( $item->user_id );

        $wform = $this;

        foreach ( $extra_cols as $xcol => $sscol ) {
            if ( $sscol >= 0 ) {
                // could be done more succinctly using $$display[$xcol]['dobject']
                // but harder to understand, and fools an ide into generating a warning
                // $rows[$n][$sscol] = Ewz_Layout::get_extra_data_item( $$display[$xcol]['dobject'], $display[$xcol]['value'], $is_auto );
                $datasource = '';
                // dont crash on undefined custom data
                if( isset( $display[$xcol] ) ){
                    switch ( $display[$xcol]['dobject'] ) {
                    case 'wform':
                        $datasource = $wform;
                        break;
                    case 'user':
                        $datasource = $user;
                        break;
                    case 'item':
                        $datasource = $item;
                        break;
                    case 'custom':
                        $datasource = $custom;
                        break;
                    default:
                        throw new EWZ_Exception( 'Invalid data source ' . $display[$xcol]['dobject'] );
                    }
                    $customrow[$sscol] = Ewz_Layout::get_extra_data_item( $datasource, $display[$xcol]['value'], $is_auto );
                }
            }
        }
        return $customrow;
    }

/**
 *
 * @param   array<Ewz_Field>      $fields
 * @param   Ewz_Item   $item
 * @return  array<int,string>      $rowlabels
 */
    private function get_label_data_for_ss( $fields, $item ) {
        $lab_fields = array_filter( $fields, function($k) { return 'lab' == $k->field_type; } );

        $rowlabels = array();
        if( $lab_fields ){
            foreach( $lab_fields as $field_id => $field ){
                $rowlabels[$field->ss_column] = $field->fdata['label'];
            }
        }
        return $rowlabels;
    }


    /**
     * Get the image file information for an item for the spreadsheet
     *
     * @param   array<Ewz_Field>     $fields     the fields for the layout
     * @param   Ewz_Item  $item    the item the image file(s) are attached to
     * @param   int       $maxcol  max column to be shown
     *
     * @return  array<string>      the spreadsheet entries for the image(s)
     */

    private function get_file_data_for_ss( $fields, $item, $maxcol ) {
        $filerow = array_fill( 0, $maxcol + 1, '' );
        if ( $item->item_files ) {
            foreach ( $item->item_files as $field_id => $item_file ) {
                $field = $fields[$field_id];
                if ( $field->ss_column >= 0 ) {
                    if ( isset( $item_file['fname'] ) ) {
                        // if the prefix was already applied, no change
                        if( $this->apply_prefix ){
                            $filerow[$field->ss_column] = basename( $item_file['fname'] );
                        } else {
                            // applying prefix/rename now
                            $filerow[$field->ss_column] = $this->get_new_filename( $fields, $item, $item_file );
                        }
                    } else {
                        $filerow[$field->ss_column] = '';
                    }
                    $this->files_done[ $filerow[$field->ss_column] ] = true;
                }
                if ( 'img' == $field->field_type ) {
                    if ( isset( $item_file['width'] ) && $field->fdata['ss_col_w'] >= 0 ) {
                        $filerow[$field->fdata['ss_col_w']] = str_replace( "\t", ' ', $item_file['width'] );
                    }
                    if ( isset( $item_file['height'] ) && $field->fdata['ss_col_h'] >= 0 ) {
                        $filerow[$field->fdata['ss_col_h']] = str_replace( "\t", ' ', $item_file['height'] );
                    }
                    if ( isset( $item_file['orient'] ) && $field->fdata['ss_col_o'] >= 0 ) {
                        $filerow[$field->fdata['ss_col_o']] = str_replace( "\t", ' ', $item_file['orient'] );
                    }
                }
            }
        }
        return $filerow;
    }

    /**
     * Get the spreadsheet entries for the uploaded data for an item
     *
     * @param   array<Ewz_Field>   $fields     the fields for the layout
     * @param   Ewz_Item             $item       the item the image file(s) are attached to
     * @param   int                  $maxcol     max column to be shown
     *
     * @return  array<string>      the spreadsheet entries for the image(s)
     */

    private function get_item_data_for_ss( $fields, $item, $maxcol ) {

        $itemrow = array_fill( 0, $maxcol + 1, '' );
        foreach ( $item->item_data as $field_id => $field_value_arr ) {
            if ( array_key_exists( $field_id, $fields ) ) {
                $field = $fields[$field_id];
                if ( $field->ss_column >= 0  && isset( $field_value_arr['value'] ) ) {
                    if( Ewz_Settings::get_ewz_option('ss_usr_labels') ){
                        $itemrow[$field->ss_column] = ewz_display_item( $field, $field_value_arr['value'] );
                    } else {
                        $itemrow[$field->ss_column] = $field_value_arr['value'];
                    }
                }
                if ( 'str' == $field->field_type ) {
                    // initcaps any "formatted text" column
                    if ( isset($field->fdata['ss_col_fmt'] ) && ($field->fdata['ss_col_fmt'] >= 0) ) {
                        // if the field is already mixed case, don't touch it
                        if ( preg_match( '/[A-Z]/', $field_value_arr['value'] ) && preg_match( '/[a-z]/', $field_value_arr['value'] ) ) {
                            $itemrow[$field->fdata['ss_col_fmt']] = $field_value_arr['value'];
                        } else {
                            $itemrow[$field->fdata['ss_col_fmt']] = ucwords( strtolower( $field_value_arr['value'] ) );
                        }
                    }
                }
                // filename columns will be set by get_file_data_for_ss
                if ( ( 'img' == $field->field_type ) &&
                isset( $field_value_arr['value'] ) &&
                ( $field_value_arr['value'] == 'ewz_img_upload' ) ) {
                    $itemrow[$field->ss_column] = '';
                }
            }
        }
        return $itemrow;
    }

    /*     * ******************  Utility Functions ********************* */

/**
 *
 * @return string
 */

    public static function get_cron_events(){
        $output = "";
        $titles =  array_column( self::get_webform_titles(), 'webform_title', 'webform_id' );
         $dateformat = get_option('date_format', 'Y-m-d');
         $tformat = get_option( 'time_format',  'H:i');
        $cron = get_option( 'cron' );
        if ( ! is_array( $cron ) ) {
            return '';
        }
        foreach ( $cron as $timestamp => $events ) {
            if( is_array($events) ){
                foreach ( $events as $event_hook => $event_args ) {
                    if( 'ewz_do_action_webform' == $event_hook ){
                        $dtime = Ewz_Base::timestamp_to_datetime( "$dateformat  $tformat", $timestamp );
                        foreach( $event_args as $key => $event_data ){
                            $output .= "<tr><td>{$dtime}:  </td><td><b>" . $event_data['args'][0] . "</b></td><td>" . $titles[$event_data['args'][1]] ."</td></tr>";
                        }
                    }
                }
            }
        }
        if( $output ){
            return "<table>$output</table>";
        } else {
            return "No webforms are scheduled to open or close automatically.";
        }
    }

/**
     * Schedule the opening or closing of this webform for uploads
     *
     * @param    string  $action 'open' or 'close'
     * @param    string  $time   time in Y-m-d H:i:s format at which to open/close the webform
     * @return   void
     */
    public function schedule_action( $action, $time ){
        if( !$this->can_manage_webform ){
            throw new EWZ_Exception( "No Permission" );
        }
        // first clear any existing $action events for this webform
        $this->unschedule_action( $action );
        $ctime = Ewz_Base::datetime_to_timestamp( $time );

        // now schedule the event
        $status = wp_schedule_single_event(  $ctime, "ewz_do_action_webform", array( $action, $this->webform_id ) );
        if( !$status ){
            throw new EWZ_Exception( "Failed to schedule $action of webform.",  $this->webform_id );
        }
    }

    /**
 *
 *
 * @param string  $action  ( 'open' or 'close' )
 *
 * @return void
 */

    public function unschedule_action( $action ){

        if( !$this->can_manage_webform ){
            throw new EWZ_Exception( "No Permission" );
        }
        $status = wp_clear_scheduled_hook( "ewz_do_action_webform", array( $action, $this->webform_id) );
        if( false === $status ){
            throw new EWZ_Exception("Failed to unschedule a '$action webform' event for webform $this->webform_id");
        }
    }


    /**
     * Add the action to schedule the close
     *
     * This function is hooked into init by entrywizard.php
     * @return void
     */

    public static function schedule_do_actions( ){
        // Hook the auto_action_webform fcn into the action ewz_do_XXX_webform
        add_action( "ewz_do_action_webform", 'Ewz_Webform::auto_action_webform', 5, 2 );
    }

    /**
     * Actually open/close the webform
     *
     * @param   string    $action     ( 'open' or 'close' )
     * @param   int    $webform_id   the webform to be opened/closed
     * @return  void
     */
    public static function auto_action_webform( $action,  $webform_id ){
        $w = new Ewz_Webform( $webform_id );
        $w->upload_open = ( $action != 'close' );
        $w->save();
        if( ( $action == 'close' ) && Ewz_Settings::get_ewz_option('gen_ssheet_on_close' ) ){
            $w->generate_spreadsheet_file();
        }
        error_log( "EWZ: webform " . $w->webform_ident . " $action done " );
    }

    /**
     * Generate a complete spreadsheet file for the webform in the image upload folder
     * Called when the webform is closed *automatically*, and the corresponding EntryWizard setting is checked.
     * @return void
     */
    public function generate_spreadsheet_file(){

        $items = Ewz_Item::get_items_for_webform( $this->webform_id, Ewz_Item::GET_ALL_ITEMS );
        $filepath = $this->download_spreadsheet( $items, self::BOTH, 'C' );

        // delete any earlier ewz_*.csv files from the folder
        $allfiles = glob( preg_replace( '/ewz_[^\/]*\.csv/', 'ewz_*.csv', $filepath ) );
        foreach ($allfiles as $found_file){
            if ( filemtime($filepath) - filemtime($found_file) >= 60 ) {
                unlink($found_file);
            }
        }
    }


    /**
     * Set the value to be displayed to the user by checking wp_next_scheduled
     * -- this value is not stored in the ewz tables
     *
     * @return  void
     */
    public function set_auto_times(){
        // $nexttime is a unix timestamp in GMT
        $nexttime = wp_next_scheduled('ewz_do_action_webform', array('close',  $this->webform_id ) );
        if( $nexttime ){
            $dateformat = get_option('date_format', 'Y-m-d');
            $tformat = get_option( 'time_format',  'H:i');
            $this->auto_close = true;
            // format
            $dt = Ewz_Base::timestamp_to_datetime("$dateformat~$tformat", $nexttime );
            list( $this->auto_close_date, $this->auto_close_time ) = explode( '~', $dt );
        } else {
            $this->auto_close = false;
            $this->auto_close_date = '';
            $this->auto_close_time = '';
        }
        $nexttime = wp_next_scheduled('ewz_do_action_webform', array( 'open', $this->webform_id ) );
        if( $nexttime ){
            $dateformat = get_option('date_format', 'Y-m-d');
            $tformat = get_option( 'time_format',  'H:i');
            $this->auto_open = true;
            // format
            $dt = Ewz_Base::timestamp_to_datetime("$dateformat~$tformat", $nexttime );
            list( $this->auto_open_date, $this->auto_open_time ) = explode( '~', $dt );
        } else {
            $this->auto_open = false;
            $this->auto_open_date = '';
            $this->auto_open_time = '';
        }
    }

    /**
     * Return an array consisting of the html options for selecting a time of day
     *
     * @param  string $selection
     *
     * @return array<int, array<string,mixed>>
     */
    public static function get_time_opt_array( $selection )
    {
        $options = array(array('value' => '', 'display' => '','selected' => false ));

        $tformat = get_option( 'time_format' );
        if( !$tformat ){
            $tformat = 'H:i:s';
        }
        for( $h=0; $h < 24; ++$h ) {

            for( $m =0; $m < 60; $m+=15 ){

                $date = DateTime::createFromFormat( "H:i:s", sprintf( "%02s:%02s:00", $h, $m ) );
                $display = $date->format( $tformat );
                $val = $display;
                if ( $selection == $display ) {
                    $is_sel = true;
                } else {
                    $is_sel = false;
                }
                array_push( $options, array( 'value' => $val,
                'display' => $display,
                'selected' => $is_sel ) );
            }
        }
        return $options;
    }

    /**
     * Get the upload directory for this webform
     *
     * Used as a filter like this to set the active upload directory:
     *      add_filter('upload_dir', 'ewz_upload_dir');
     *      ... do the upload ...
     *      remove_filter('upload_dir', 'ewz_upload_dir');
     *
     * @param   array<string> $upload_data  default upload_dir (output of wp_upload_dir()) with components "dir" "url" "error"
     * @return  array<string>  altered value of input array
     */
    public function ewz_upload_dir( $upload_data ) {
        $upload_data['subdir'] = '/' . EWZ_IMG_UPLOAD_SUBDIR . '/' . $this->webform_ident;
        $upload_data['path'] = EWZ_IMG_UPLOAD_DIR . '/' . $this->webform_ident;
        $upload_data['url'] = EWZ_IMG_UPLOAD_URL . '/' . $this->webform_ident;
        return $upload_data;
    }

    /**
     * Is the webform open for upload by the current user
     *
     * @return boolean
     */
    public function open_for_current_user() {
        if ( $this->upload_open ) {
            return true;
        } elseif ( in_array( get_current_user_id(), $this->open_for ) ) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Make substitutions in the prefix for some expressions
     * When adding prefix on upload, is called with $all_fields false or absent
     * When adding prefix on download, is called with $all_fields true
     *
     * @param   array<string,mixed>   $data    data required to generate the substitute values
     * @param   bool    $all_fields
     * @return  string  $prefix
     */
    public function generated_prefix( $data, $all_fields=false ) {
        if( $this->prefix ){
            if( isset($data['item_id'] ) ){
                $item_id = $data['item_id'];
            } else {
                $item_id =0;
            }
            $placeholders = array( '[~UID]',         '[~WFM]',                    '[~FLD]',           '[~RAND]' );
            $replacements = array( $data['user_id'], $this->webform_ident,  $data['file_field_id'], $this->get_rand($item_id ));

            for ( $n = 1; $n <= 9; ++$n ) {
                if ( isset( $data["custom$n"] ) ) {
                    array_push( $placeholders, '[~CD' . $n . ']' );
                    array_push( $replacements, $data['custom' . $n] );
                }
            }
            foreach(  Ewz_Field::get_fields_for_layout( $this->layout_id, 'ss_column' ) as $field ){
                if( ( $all_fields || in_array( $field->field_type, array( 'opt', 'rad', 'chk' ) ) ) && $field->field_ident != 'followupQ' ){
                    $val = '';
                    if( isset ( $data[$field->field_id]['value'] ) ){
                        $val = $data[$field->field_id]['value'];
                        array_push( $replacements, $val );
                    } else {
                        array_push( $replacements, '' );
                    }
                    array_push( $placeholders, '[~' . $field->field_ident . ']' );
                }
            }
            $out_prefix = str_replace( $placeholders, $replacements, $this->prefix );
            return $out_prefix;
        } else {
            return $this->prefix;
        }
    }

    /**
     * Generate a random number. If item_id is set (ie generating prefix on download),
     * save the random number for re-use for the same item_id -- so filename matches content of spreadsheet.
     *
     * @param int $item_id
     *
     * @return string representing random number
     */
    private function get_rand( $item_id )
    {
        if( $item_id ){
            // generating prefix on download
            if( !isset( $this->randmap[$item_id] ) ){
                $this->randmap[$item_id] = sprintf('%03d', rand (0, 999 ) );
            }
            return $this->randmap[$item_id];
        } else {
            // uploading, the random number will be included in the filename on the server, no need to save it.
            return sprintf('%03d', rand (0, 999 ) );
        }
    }

    /**
     *
     * @return string
     */
    public function get_user_list(){
        $user_options =  Ewz_User::get_user_opt_array(false);   // false == don't include username and email
        foreach ( $this->open_for as $user ) {
            foreach( $user_options as $n => $u_options ){
                if( $u_options['value'] == $user ){
                    $user_options[$n]['selected'] = true;
                }
            }
        }
        return  ewz_option_list( $user_options );
    }

    /**
     *
     * @return void
     */
    public function add_display_info( ){

        // easier in Javascript if this is set
        $this->layout_name = esc_html( $this->layout->layout_name );
        $this->canOverride = $this->layout->override;

        // generate the options lists for each field of the selected layout
        foreach ( $this->layout->fields as $field ) {
            $this->field_idents[$field->field_id] = $field->field_ident;
            $this->field_types[$field->field_id] = $field->field_type;
            $this->field_names[$field->field_id] = ewz_html_esc( $field->field_header );
            $this->field_options[$field->field_id] = ewz_option_list( $field->get_field_list_array() );
        }
        if( method_exists( 'Ewz_Custom_Data', 'selection_list' ) ){
            foreach ( Ewz_Custom_Data::$data as $key => $name ){
                $list = Ewz_Custom_Data::selection_list( $key );
                if( $list ){
                    $opts = array(array('value'=>'~*~', 'display'=> 'Any', 'selected' => true));
                    $this->custom_header[$key] = $name;
                    foreach( $list as $key1 => $val ){
                        $x =  array( 'value' => $val, 'display' => $val );
                        array_push( $opts, $x );
                    }
                    $this->custom_options[$key] = ewz_option_list( $opts );
                }
            }
        }
        $l_options = Ewz_Layout::get_layout_opt_array( 'can_assign_layout', $this->layout_id );
        $this->layouts_options = ewz_option_list( $l_options  );
        $this->time_opts_close = ewz_option_list( self::get_time_opt_array( $this->auto_close_time)  );
        $this->time_opts_open = ewz_option_list( self::get_time_opt_array( $this->auto_open_time)  );
    }

    /*     * ******************  Validation  ****************************** */

    /**
     * Check for various error conditions and throw an exception when one is found
     *
     * @return  boolean true on success
     */
    protected function check_errors() {
        global $wpdb;
        if ( is_string( $this->open_for ) ) {/** @phpstan-ignore-line */
            $this->open_for = self::check_unserial_arr( $this->open_for );
        }
        foreach ( self::$varlist as $key => $type ) {
            settype( $this->$key, $type );
        }
        if ( !isset( $this->webform_title ) ) {
            throw new EWZ_Exception( 'A webform must have a title' );
        }
        if ( !isset( $this->webform_ident ) ) {
            throw new EWZ_Exception( 'A webform must have an identifier' );
        }
        $used1 = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_title = %s AND webform_id != %d", $this->webform_title, $this->webform_id ) );
        if ( $used1 > 0 ) {
            throw new EWZ_Exception( "Webform title  '$this->webform_title' is already in use" );
        }
        $used2 = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_WEBFORM_TABLE .
        " WHERE webform_ident = %s AND webform_id != %d", $this->webform_ident, $this->webform_id ) );
        if ( $used2 > 0 ) {
            throw new EWZ_Exception( 'Webform identifier ' . $this->webform_ident . ' is already in use' );
        }
        if( strlen( $this->webform_title ) > 100 ){
            throw new EWZ_Exception( 'Webform title too long');
        }
        if( strlen( $this->webform_ident ) > 15 ){
            throw new EWZ_Exception( 'Webform identifier too long');
        }
        if( strlen( $this->prefix ) > 75 ){
            throw new EWZ_Exception( 'Webform prefix too long');
        }
        return true;
    }

    /**
     * Get the saved options for attaching images to pages
     *
     * Returned array has values for  'ewz_page_sel' -- id of the selected page,
     *                                'img_size' -- selected image size option,
     *                                'img_qual' -- override of wp default jpeg quality,
     *                                'img_comment' -- boolean, allow comments on attached image
     *                                'dups_ok' -- boolean, allow images to be attached more than once to the same page
     * If there is more than one image field, it may also include a value for 'ifield' -- the image column to use
     *
     * @return  array<string,mixed> of attachment preferences
     */
    public function get_attach_prefs() {
        global $wpdb;

        $base_prefs = array( 'ewz_page_sel' => 0,
        'img_size' => Ewz_Settings::get_ewz_option('attach_img_size'),
        'img_qual' => Ewz_Settings::get_ewz_option('attach_jpeg_qual'),
        'img_comment' => Ewz_Settings::get_ewz_option('attach_img_comment'),
        'dups_ok' => false
        );
        $wf_prefs = $base_prefs;
        if ( $this->webform_id ) {

            $wf_tmp =  $wpdb->get_var( $wpdb->prepare( "SELECT attach_prefs FROM " . EWZ_WEBFORM_TABLE .
            " WHERE  webform_id = %d", $this->webform_id ) );
            if( $wf_tmp ){
                $wf_prefs = self::check_unserial_arr( $wf_tmp );
                if( !isset($wf_prefs['dups_ok']) ){
                    $wf_prefs = $base_prefs;
                }
            }
        }
        return $wf_prefs;
    }

    /**
     * @param string  $filter  optional Ewz_Permission function used to filter the webforms
     *
     * @return array<int, array<string,mixed>>
     */
    public static function get_hidden_webforms_opt_array( $filter = 'truefunc' ){
        global $wpdb;
        $options = array();

        $list = $wpdb->get_results( "SELECT webform_id, webform_title  FROM " . EWZ_WEBFORM_TABLE . " WHERE hidden = 1 ORDER BY webform_title" );  // no tainted data
        foreach( $list as $n => $data ){
            if ( call_user_func( array( 'Ewz_Permission',  $filter ), $data->webform_id ) ) {
                array_push( $options, array( 'value' => $data->webform_id ,
                'display' => esc_html( $data->webform_title ),
                'selected' => false )
                );
            }
        }
        return $options;
    }

    /*     * ******************  Database Updates ********************* */

    /**
     * Save the order of the webforms. Admins save viewed order unconditionally.
     * Others may only save relative order of items they are allowed to edit.
     *
     * @param   array<string,mixed>    $wf_orders
     * @param   boolean  $resave_existing
     *
     * @return  int      number of rows updated
     */
    public static function update_webform_order( $wf_orders, $resave_existing = false ) {
        global $wpdb;
        // cache_no_change~wforms_for_layout~
        $is_admin = Ewz_Permission::can_edit_all_webforms();

        $stored_orders = self::get_webform_order( true );
        //  resave the existing orders, with omissions and duplications fixed
        if( $resave_existing ){
            $wf_orders = $stored_orders;
            $allowed_orders = array_values( $stored_orders );
        } else {
            if( $is_admin ){
                $allowed_orders =  array_values( $wf_orders );
                sort( $allowed_orders);
            } else {
                $orig_order = array_intersect_key( $stored_orders, $wf_orders );
                $allowed_orders = array_values( $orig_order);
                sort( $allowed_orders);
            }
        }
        $n = 0;
        foreach( $wf_orders as $webform_id => $order ){
            $n = $n + $wpdb->query($wpdb->prepare("UPDATE " . EWZ_WEBFORM_TABLE . " wf " .
            "   SET webform_order = %d WHERE webform_id = %d ", $allowed_orders[$order], $webform_id ) );
            if( $wpdb->last_error ){
                throw new EWZ_Exception( 'Saving of webform order failed', $wpdb->last_error );
            }
        }
        return $n;
    }


    /**
     * Set the "hidden" flag to $status  ( 1 = hidden, 0 = visible )
     *
     * @param boolean  $hide
     * @return void
     **/
    public function set_hidden_status( $hide ){
        global $wpdb;

        if ( !$this->can_edit_webform && !defined( 'DOING_CRON' )) {
            throw new EWZ_Exception( "No changes saved. Insufficient permissions to change webform '$this->webform_title' )" );
        }
        $newvals =  array( 'hidden' => ( $hide ? 1 : 0 ));
        if( $hide == 1 ){
            // force hidden webform to end of list
            $newvals['webform_order'] = EWZ_MAX_VIZ;
        } else {
            // force newly unhidden webform to start of list
            $newvals['webform_order'] = 0;
        }

        // cache_no_change~wforms_for_layout~
        $status = $wpdb->update( EWZ_WEBFORM_TABLE,                              // no esc
        $newvals,
        array( 'webform_id' => $this->webform_id ),
        array( '%d' ),
        array( '%d' ) );
        if( false === $status || $wpdb->last_error ){
            throw  new EWZ_Exception( 'Failed to update "hidden" status for webform ' . $this->webform_title );
        }
        self::update_webform_order( array(), true);
    }

    /**
     * Set num_items to the input value
     *
     * @param int  $numitems
     * @return void
     **/
    public function update_num_items( $numitems ){

        // no permission check here, called only from layout when number changed
        global $wpdb;
        // cache_no_change~wforms_for_layout~
        $status = $wpdb->update( EWZ_WEBFORM_TABLE, array( 'num_items' => (int)$numitems ), array( 'webform_id' => $this->webform_id ), array( '%d' ), array( '%d' ) );  // no esc
        if( false === $status || $wpdb->last_error ){
            throw  new EWZ_Exception( 'Failed to update max number of items for webform ' . $this->webform_title );
        }
    }

    /**
     * @return string message
     */
    public function delete_all_items(){
        if ( !$this->can_edit_webform && !defined( 'DOING_CRON' )) {
            throw new EWZ_Exception( "No changes saved. Insufficient permissions to change webform '$this->webform_title' )" );
        }
        $all_items = Ewz_Item::get_items_for_webform( $this->webform_id,  Ewz_Item::GET_ALL_ITEMS);
        $fileErrMsg = '';
        $itemsDeleted = 0;
        foreach ( $all_items as $item ) {
            try {
                $ferr = $item->delete();  // no arg defaults to delete files, too
                ++$itemsDeleted;
                if( $ferr ) {
                    $fileErrMsg .= "\n$ferr" ;
                }
            } catch( EWZ_Exception $e ) {
                throw new EWZ_Exception( "Data was deleted for $itemsDeleted items. $fileErrMsg.  \n***ERROR: " . $e->getMessage() );
            }
        }
        // delete any remaining .csv files
        $otherDeleted = 0;
        if ( $dh = opendir( EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident )) {
            while (($file = readdir($dh)) !== false) {
                if( !preg_match( '/^\./', $file ) && (preg_match( '/\.csv$|\.zip/', $file  )  ) ){
                    unlink(  EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident . "/$file" );
                    ++$otherDeleted;
                }
            }
            closedir($dh);
        }
        return "All data and files were deleted for $itemsDeleted items. $fileErrMsg";
    }

    /**
     * @return string message
     */
   public function delete_zipfiles(){
        if ( !$this->can_edit_webform && !defined( 'DOING_CRON' )) {
            throw new EWZ_Exception( "No changes saved. Insufficient permissions to change webform '$this->webform_title' )" );
        }
        $otherDeleted = 0;
        $now = time();
        if ( $dh = opendir( EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident )) {
            while (($file = readdir($dh)) !== false) {
                if( !preg_match( '/^\./', $file ) &&
                preg_match( '/\.csv$|\.zip/', $file )  &&
                ( $now - filemtime( $file ) > 60) ){   // 1 minute -- this only runs when deliberately selected
                    unlink(  EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident . "/$file" );
                    ++$otherDeleted;
                }
            }
            closedir($dh);
        }
        return "$otherDeleted files removed.";
    }



    /**
     * Save the options for attaching images to pages
     *
     * Input  array should have values for  'ewz_page_sel' -- id of the selected page,
     *                                      'img_size' -- selected image size option,
     *                                      'img_comment' -- boolean, allow comments on attached image
     *                                      'dups_ok' -- boolean, allow an image to be attached more than once
     *
     * @param  array<string,mixed>  $prefs of attachment preferences
     * @return void
     */
    public function save_attach_prefs( $prefs ) {
        global $wpdb;

        if ( $this->webform_id ) {

            if( !$this->can_manage_webform ) {
                throw new EWZ_Exception( 'No permission to manage webform' );
            }
            // cache_no_change~wforms_for_layout~
            $data =  array( 'attach_prefs' => ewz_serialize( stripslashes_deep($prefs ), 1000 ) );
            $rows = $wpdb->update( EWZ_WEBFORM_TABLE, $data, array( 'webform_id' => $this->webform_id ), array( '%s' ), array( '%d' ) ); // no esc
            if ( $rows > 1 || $wpdb->last_error ) {
                throw new EWZ_Exception( "Problem setting attach options for the webform '$this->webform_title' " .  $wpdb->last_error );
            }
        }
    }

    /**
     * Save the webform to the database
     *
     * Check for permissions, then update or insert the webform data
     *
     * @return int webform_id of saved webform
     */
    public function save() {
        global $wpdb;
        if ( $this->webform_id ) {
            // check the old layout_id against the new -- if they are different, need the right permission
            $old_webform = new Ewz_Webform( $this->webform_id );

            if ( ( $old_webform->layout_id !== $this->layout_id ) &&  !$this->can_edit_webform ) {
                throw new EWZ_Exception( 'No changes saved. Insufficient permissions to change layout ', $old_webform->layout_id );
            }
            if ( !$this->can_manage_webform && !defined( 'DOING_CRON' )) {
                throw new EWZ_Exception( "No changes saved. Insufficient permissions to change webform '$this->webform_title' )" );
            }
        } else {
            if ( !Ewz_Permission::can_edit_all_webforms() ) {
                throw new EWZ_Exception( 'No changes saved. Insufficient permissions to create a webform' );
            }
        }
        // ok, we have all the permissions, go ahead

        $this->check_errors();

        wp_cache_delete( $this->layout_id, 'wforms_for_layout' );

        // NB: webform_order is not updated here
        $data = stripslashes_deep( array(
            'layout_id' => $this->layout_id,               // %d
            'num_items' => $this->num_items,               // %d
            'webform_title' => $this->webform_title,       // %s
            'webform_ident' => $this->webform_ident,       // %s
            'upload_open' => $this->upload_open ? 1 : 0,   // %d
            'open_for' => ewz_serialize( $this->open_for, 1000 ),    // %s
            'prefix' => $this->prefix,                     // %s
            'apply_prefix' => $this->apply_prefix ? 1 : 0, // %d
            'gen_fname' => $this->gen_fname ? 1 : 0,       // %d
        ) );
        $datatypes = array( '%d',   // = layout_id
        '%d',   // = num_items
        '%s',   // = webform_title
        '%s',   // = webform_ident
        '%d',   // = upload_open
        '%s',   // = open_for
        '%s',   // = prefix
        '%d',   // = apply_prefix
        '%d',   // = gen_fname
        );

        if ( $this->webform_id ) {
            $rows = $wpdb->update( EWZ_WEBFORM_TABLE,                                                       // no esc
            $data,       array( 'webform_id' => $this->webform_id ),
            $datatypes,  array( '%d' ) );
            if( false === $rows || $wpdb->last_error ){
                throw new EWZ_Exception( 'Failed to update webform '. $wpdb->last_error );
            }
            if ( $rows > 1 ) {
                throw new EWZ_Exception( "Problem updating the webform '$this->webform_title'" );
            }
        } else {
            $n_webforms = self::count_visible_webforms();
            $rowsaffected = $wpdb->insert( EWZ_WEBFORM_TABLE, $data, $datatypes );                       // no esc
            if( ( false === $rowsaffected ) || ( $rowsaffected != 1 || $wpdb->last_error )){
                throw new EWZ_Exception( 'Creation of webform failed. '. $wpdb->last_error );
            }
            $this->webform_id = $wpdb->insert_id;
            if ( !$this->webform_id ) {
                throw new EWZ_Exception( "Problem creating the webform '$this->webform_title'");
            }

            $rows = $wpdb->query($wpdb->prepare( "UPDATE " . EWZ_WEBFORM_TABLE .
            "   SET webform_order = %d WHERE  webform_id = %d ", $n_webforms, $this->webform_id ) );
            if( false === $rows || $wpdb->last_error ){  /** @phpstan-ignore-line */
                throw new EWZ_Exception( 'Update of webform order failed. ' . $wpdb->last_error );
            }
        }
        return $this->webform_id;
    }

    /**
     * Delete the webform from the database
     * First calls the action ewz_before_delete_webform
     *
     * @param  int  $delete_items     either FAIL_IF_ITEMS or DELETE_ITEMS
     * @return string  message
     */
    public function delete( $delete_items = self::FAIL_IF_ITEMS ) {
        global $wpdb;
        if ( $this->webform_id ) {
            if ( !Ewz_Permission::can_edit_all_webforms() ) {
                throw new EWZ_Exception( "No changes saved. Insufficient permissions to delete webform '$this->webform_title'" );
            }
            try{
                do_action( 'ewz_before_delete_webform', $this->webform_id );
            } catch( Exception $e ){
                // bail now, before the deletion gets done
                throw new EWZ_Exception( 'Cannot delete webform: ' . $e->getMessage() );
            }

            $delete_msg = '';
            if ( $delete_items == self::DELETE_ITEMS ) {
                try{
                    $delete_msg = $this->delete_all_items();  // also deletes any .csv/zip files
                } catch( Exception $e ){
                    throw new EWZ_Exception( 'Error deleting webform items: ' . $e->getMessage() );
                }
            } else {
                $n = count(  Ewz_Item::get_items_for_webform( $this->webform_id,  Ewz_Item::GET_ALL_ITEMS) );
                if ( $n > 0 ) {
                    throw new EWZ_Exception( "Webform has $n items attached." );
                }
            }
            // if we get here, there should be no items left in the folder
            // delete any remaining zip or .csv files, and the index file, then delete the folder itself
            try{
                if ( $dh = opendir( EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident )) {
                    while (($file = readdir($dh)) !== false) {
                        if( !preg_match( '/^\./', $file ) && (preg_match( '/\.csv$|\.zip/', $file  )  ) ){
                            unlink(  EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident . "/$file" );
                        }
                        if( $file == 'index.php' ){
                            unlink(  EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident . "/$file" );
                        }
                    }
                    closedir($dh);
                } else {
                    error_log( "EWZ: Webform $this->webform_ident: folder could not be opened to delete." );
                }
                rmdir( EWZ_IMG_UPLOAD_DIR . "/" . $this->webform_ident );

            } catch( Exception $e ){
                throw new EWZ_Exception( 'Error deleting files from webform: ' . $e->getMessage() );
            }

            wp_cache_delete( $this->layout_id, 'wforms_for_layout' );

            // actually delete the webform from the database
            $rowsaffected = $wpdb->query( $wpdb->prepare( "DELETE FROM " . EWZ_WEBFORM_TABLE . " WHERE webform_id = %d", $this->webform_id ) );

            if( false === $rowsaffected || $wpdb->last_error){
                throw new EWZ_Exception( "Failed to delete webform '$this->webform_title '" . $wpdb->last_error );
            }

            if ( 1 == $rowsaffected ) {
                self::update_webform_order( array(), true);
                // do this after the deletion, so any failures in it don't stop the deletion
                Ewz_Permission::delete_webform_permissions( $this->webform_id );
                return "1 webform deleted.\n$delete_msg ";
            } else {
                throw new EWZ_Exception( "Problem deleting webform '$this->webform_title '" );
            }
        } else {
            throw new EWZ_Exception( "Problem deleting webform: No webform specified " );
        }

    }

    /*     * ******************  Utility Functions ********************* */

    /**
     * Return a random string of lower-case alpha, upper-case alpha or integer characters
     *
     * @param  int   $len    length of string to return
     * @return string
     */
    private function randstring( $len ){

        return implode('', array_map( function () {
            $type = rand(0, 2);
            switch ( $type ){
            case 0:  return chr( mt_rand(48, 57) );
                // break;
            case 1: return chr(mt_rand(97, 122) );
                // break;
            case 2: return chr( mt_rand(65, 90) );
                // break;
            default: return chr(mt_rand(97, 122) );
            }
        },
        range(0, $len - 1) ));
    }

    /**
     * Stop output buffering and compression
     * @return void
     */
    private static function ewz_disable_gzip(){
        if( extension_loaded( 'zlib' ) ){
            ini_set('zlib.output_compression', '0' );
        }
        if( ini_get( 'output_buffering' ) ){
            ini_set( 'output_buffering',  '0' );
        }
        if( function_exists( 'apache_setenv' ) ){
            apache_setenv( 'no-gzip', '1' );
        }
    }
}
