<?php if ( ! defined( 'ABSPATH' ) ) exit;

class NF_AJAX_Controllers_Submission extends NF_Abstracts_Controller
{
    protected $_form_data = array();

    protected $_form_cache = array();

    protected $_preview_data = array();

    protected $_form_id = '';

    public function __construct()
    {
        if( isset( $_POST[ 'nf_resume' ] ) && isset( $_COOKIE[ 'nf_wp_session' ] ) ){
            add_action( 'init', array( $this, 'resume' ) );
            return;
        }

        if( isset( $_POST['formData'] ) ) {
            $this->_form_data = json_decode( $_POST['formData'], TRUE  );

            // php5.2 fallback
            if( ! $this->_form_data ) $this->_form_data = json_decode( stripslashes( $_POST['formData'] ), TRUE  );
        }


        // Ajax calls here are both handled by 'submit' in this file
        add_action( 'wp_ajax_nf_ajax_submit',   array( $this, 'submit' )  );
        add_action( 'wp_ajax_nopriv_nf_ajax_submit',   array( $this, 'submit' )  );

        /**
         * Ajax calls here are handled by 'resume' in this file. These calls
         * are normally made by the application when returning from 'PayPal' or
         * 'Stripe'.
         */
        add_action( 'wp_ajax_nf_ajax_resume',   array( $this, 'resume' )  );
        add_action( 'wp_ajax_nopriv_nf_ajax_resume',   array( $this, 'resume' )  );
    }

    public function submit()
    {
    	$nonce_name = 'ninja_forms_display_nonce';
    	/**
	     * We've got to get the 'nonce_ts' to append to the nonce name to get
	     * the unique nonce we created
	     * */
    	if( isset( $_REQUEST[ 'nonce_ts' ] ) && 0 < strlen( $_REQUEST[ 'nonce_ts' ] ) ) {
    		$nonce_name = $nonce_name . "_" . $_REQUEST[ 'nonce_ts' ];
	    }
        $check_ajax_referer = check_ajax_referer( $nonce_name, 'security', $die = false );
        if(!$check_ajax_referer){
            /**
             * "Just in Time Nonce".
             * If the nonce fails, then send back a new nonce for the form to resubmit.
             * This supports the edge-case of 11:59:59 form submissions, while avoiding the form load nonce request.
             */

            $current_time_stamp = time();
            $new_nonce_name = 'ninja_forms_display_nonce_' . $current_time_stamp;
            $this->_errors['nonce'] = array(
                'new_nonce' => wp_create_nonce( $new_nonce_name ),
                'nonce_ts' => $current_time_stamp
            );
            $this->_respond();
        }

        register_shutdown_function( array( $this, 'shutdown' ) );

        $this->form_data_check();

        $this->_form_id = $this->_form_data['id'];

        /* Render Instance Fix */
        if(strpos($this->_form_id, '_')){
            $this->_form_instance_id = $this->_form_id;
            list($this->_form_id, $this->_instance_id) = explode('_', $this->_form_id);
            $updated_fields = array();
            foreach($this->_form_data['fields'] as $field_id => $field ){
                list($field_id) = explode('_', $field_id);
                list($field['id']) = explode('_', $field['id']);
                $updated_fields[$field_id] = $field;
            }
            $this->_form_data['fields'] = $updated_fields;
        }
        /* END Render Instance Fix */

        // If we don't have a numeric form ID...
        if ( ! is_numeric( $this->_form_id ) ) {
            // Kick the request out without processing.
            $this->_errors[] = esc_html__( 'Form does not exist.', 'ninja-forms' );
            $this->_respond();
        }

        // Check to see if our form is maintenance mode.
        $is_maintenance = WPN_Helper::form_in_maintenance( $this->_form_id );

        /*
         * If our form is in maintenance mode then, stop processing and throw an error with a link
         * back to the form.
         */
        if ( $is_maintenance ) {
            $this->_errors[ 'form' ][] = apply_filters( 'nf_maintenance_message', esc_html__( 'This form is currently undergoing maintenance. Please ', 'ninja-forms' )
                . '<a href="' . $_SERVER[ 'HTTP_REFERER' ] . '">' . esc_html__( 'click here ', 'ninja-forms' ) . '</a>' . esc_html__( 'to reload the form and try again.', 'ninja-forms' )  ) ;
            $this->_respond();
        }

        if( $this->is_preview() ) {

            $this->_form_cache = get_user_option( 'nf_form_preview_' . $this->_form_id );

            if( ! $this->_form_cache ){
                $this->_errors[ 'preview' ] = esc_html__( 'Preview does not exist.', 'ninja-forms' );
                $this->_respond();
            }
        } else {
            if( WPN_Helper::use_cache() ) {
                $this->_form_cache = WPN_Helper::get_nf_cache( $this->_form_id );
            }

        }

        // Add Field Keys to _form_data
        if(! $this->is_preview()){
            $form_fields = Ninja_Forms()->form($this->_form_id)->get_fields();
            foreach ($form_fields as $id => $field) {
                $this->_form_data['fields'][$id]['key'] = $field->get_setting('key');
            }
        }

        // TODO: Update Conditional Logic to preserve field ID => [ Settings, ID ] structure.
        $this->_form_data = apply_filters( 'ninja_forms_submit_data', $this->_form_data );

        $this->process();
    }

    public function resume()
    {
        $this->_form_data = Ninja_Forms()->session()->get( 'nf_processing_form_data' );
        $this->_form_cache = Ninja_Forms()->session()->get( 'nf_processing_form_cache' );
        $this->_data = Ninja_Forms()->session()->get( 'nf_processing_data' );
        $this->_data[ 'resume' ] = WPN_Helper::sanitize_text_field($_POST[ 'nf_resume' ]);

        $this->_form_id = $this->_data[ 'form_id' ];

        unset( $this->_data[ 'halt' ] );
        unset( $this->_data[ 'actions' ][ 'redirect' ] );

        $this->process();
    }

    protected function process()
    {
        // Init Field Merge Tags.
        $field_merge_tags = Ninja_Forms()->merge_tags[ 'fields' ];
        $field_merge_tags->set_form_id( $this->_form_id );

        // Init Calc Merge Tags.
        $calcs_merge_tags = Ninja_Forms()->merge_tags[ 'calcs' ];

        if(isset($this->_form_cache[ 'settings' ] ) ) {
            $form_settings = $this->_form_cache[ 'settings' ];
        } else {
            $form_settings = false;
        }

        if( ! $form_settings ){
            $form = Ninja_Forms()->form( $this->_form_id )->get();
            $form_settings = $form->get_settings();
        }

	    // Init Form Merge Tags.
	    $form_merge_tags = Ninja_Forms()->merge_tags[ 'form' ];
	    $form_merge_tags->set_form_id( $this->_form_id );
	    $form_merge_tags->set_form_title( $form_settings['title'] );

        $this->_data[ 'form_id' ] = $this->_form_data[ 'form_id' ] = $this->_form_id;
        $this->_data[ 'settings' ] = $form_settings;
        $this->_data[ 'settings' ][ 'is_preview' ] = $this->is_preview();
        $this->_data[ 'extra' ] = $this->_form_data[ 'extra' ];

        /*
        |--------------------------------------------------------------------------
        | Fields
        |--------------------------------------------------------------------------
        */

        if( $this->is_preview() ){
            $form_fields = $this->_form_cache[ 'fields' ];
        } else {
            $form_fields = Ninja_Forms()->form($this->_form_id)->get_fields();
        }

        /**
         * The Field Processing Loop.
         *
         * There can only be one!
         * For performance reasons, this should be the only time that the fields array is traversed.
         * Anything needing to loop through fields should integrate here.
         */
        $validate_fields = apply_filters( 'ninja_forms_validate_fields', true, $this->_data );
        foreach( $form_fields as $key => $field ){

            if( is_object( $field ) ) {
                $field = array(
                    'id' => $field->get_id(),
                    'settings' => $field->get_settings()
                );
            }

            /** Get the field ID */
            /*
             * TODO: Refactor data structures to match.
             * Preview: Field IDs are stored as the associated array key.
             * Publish: Field IDs are stored as an array key=>value pair.
             */
            if( $this->is_preview() ){
                $field[ 'id' ] = $key;
            }

            // Duplicate field ID as single variable for more readable array access.
            $field_id = $field[ 'id' ];

            // Check that the field ID exists in the submitted for data and has a submitted value.
            if( isset( $this->_form_data[ 'fields' ][ $field_id ] ) && isset( $this->_form_data[ 'fields' ][ $field_id ][ 'value' ] ) ){
                $field[ 'value' ] = $this->_form_data[ 'fields' ][ $field_id ][ 'value' ];
            } else {
                $field[ 'value' ] = '';
            }

            // Duplicate field value to settings and top level array item for backwards compatible access (ie Save Action).
            $field[ 'settings' ][ 'value' ] = $field[ 'value' ];

            // Duplicate field value to form cache for passing to the action filter.
            $this->_form_cache[ 'fields' ][ $key ][ 'settings' ][ 'value' ] = $this->_form_data[ 'fields' ][ $field_id ][ 'value' ];

            // Duplicate the Field ID for access as a setting.
            $field[ 'settings' ][ 'id' ] = $field[ 'id' ];

            // Combine with submitted data.
            $field = array_merge( $field, $this->_form_data[ 'fields' ][ $field_id ] );

            // Flatten the field array.
            $field = array_merge( $field, $field[ 'settings' ] );

            /** Validate the Field */
            if( $validate_fields && ! isset( $this->_data[ 'resume' ] ) ){
                $this->validate_field( $field );
            }

            /** Process the Field */
            if( ! isset( $this->_data[ 'resume' ] ) ) {
                $this->process_field($field);
            }
            $field = array_merge( $field, $this->_form_data[ 'fields' ][ $field_id ] );

	        // Check for field errors after processing.
	        if ( isset( $this->_form_data['errors']['fields'][ $field_id ] ) ) {
		        $this->_errors['fields'][ $field_id ] = $this->_form_data['errors']['fields'][ $field_id ];
		        $this->_respond();
	        }

            /** Populate Field Merge Tag */
            $field_merge_tags->add_field( $field );

            $this->_data[ 'fields' ][ $field_id ] = $field;
            $this->_data[ 'fields_by_key' ][ $field[ 'key' ] ] = $field;
        }

        /*
        |--------------------------------------------------------------------------
        | Check for unique field settings.
        |--------------------------------------------------------------------------
        */
        if ( isset( $this->_data[ 'resume' ] ) && $this->_data[ 'resume' ] ){
            // On Resume Submission, we don't need to run this check again.
            // This section intentionally left blank.
        } elseif ( isset ( $this->_data[ 'settings' ][ 'unique_field' ] ) && ! empty( $this->_data[ 'settings' ][ 'unique_field' ] ) ) {
            /*
             * Get our unique field
             */
            $unique_field_key = $this->_data[ 'settings' ][ 'unique_field' ];
            $unique_field_error = $this->_data[ 'settings' ][ 'unique_field_error' ];
            $unique_field_id = $this->_data[ 'fields_by_key' ][ $unique_field_key ][ 'id' ];
            $unique_field_value = $this->_data[ 'fields_by_key' ][ $unique_field_key ][ 'value' ];
            if ( is_array( $unique_field_value ) ) {
                $unique_field_value = serialize( $unique_field_value );
            }

            if ( ! empty($unique_field_value) ) {
                /*
                 * Check our db for the value submitted.
                 */
                
                global $wpdb;
                // @TODO: Rewrite this to use our submissions API.
                $sql = $wpdb->prepare( "SELECT COUNT(m.meta_id) FROM `" . $wpdb->prefix . "postmeta` AS m LEFT JOIN `" . $wpdb->prefix . "posts` AS p ON p.id = m.post_id WHERE m.meta_key = '_field_%d' AND m.meta_value = '%s' AND p.post_status = 'publish'", $unique_field_id, $unique_field_value );
                $result = $wpdb->get_results( $sql, 'ARRAY_N' );
                if ( intval( $result[ 0 ][ 0 ] ) > 0 ) {
                    $this->_errors['fields'][ $unique_field_id ] = array( 'slug' => 'unique_field', 'message' => $unique_field_error );
                    $this->_respond();
                }
            }
        }

        /*
        |--------------------------------------------------------------------------
        | Verify the submission limit.
        |--------------------------------------------------------------------------
        */
        if ( isset( $this->_data[ 'settings' ][ 'sub_limit_number' ] ) && ! empty( $this->_data[ 'settings' ][ 'sub_limit_number' ] ) ) {
            $subs = Ninja_Forms()->form( $this->_form_id )->get_subs();
            if ( count( $subs ) >= intval( $this->_data[ 'settings' ][ 'sub_limit_number' ] ) ) {
                $this->_errors[ 'form' ][] = $this->_data[ 'settings' ][ 'sub_limit_msg' ];
                $this->_respond();
            }
        }

        /*
        |--------------------------------------------------------------------------
        | Calculations
        |--------------------------------------------------------------------------
        */

        if( isset( $this->_form_cache[ 'settings' ][ 'calculations' ] ) ) {

            /**
             * The Calculation Processing Loop
             */
            foreach( $this->_form_cache[ 'settings' ][ 'calculations' ] as $calc ){
                $eq = apply_filters( 'ninja_forms_calc_setting', $calc[ 'eq' ] );

                // Scrub unmerged tags (ie deleted/non-existent fields/calcs, etc).
                $eq = preg_replace( '/{([a-zA-Z0-9]|:|_|-)*}/', 0, $eq);

				/**
				 * PHP doesn't evaluate empty strings to numbers. So check
	             * for any string for the decimal place
				**/
                $dec = ( isset( $calc[ 'dec' ] ) && '' != $calc[ 'dec' ] ) ?
	                $calc[ 'dec' ] : 2;
                
                $calcs_merge_tags->set_merge_tags( $calc[ 'name' ], $eq, $dec, $this->_form_data['settings']['decimal_point'], $this->_form_data['settings']['thousands_sep'] );
                $this->_data[ 'extra' ][ 'calculations' ][ $calc[ 'name' ] ] = array(
                    'raw' => $calc[ 'eq' ],
                    'parsed' => $eq,
                    'value' => $calcs_merge_tags->get_formatted_calc_value( $calc[ 'name' ], $dec, $this->_form_data['settings']['decimal_point'], $this->_form_data['settings']['thousands_sep'] ),
                );
            }
        }


        /*
        |--------------------------------------------------------------------------
        | Actions
        |--------------------------------------------------------------------------
        */

        /*
         * TODO: This section has become convoluted, but will be refactored along with the submission controller.
         */

        if( isset( $this->_data[ 'resume' ] ) && $this->_data[ 'resume' ] ){
            // On Resume Submission, the action data is loaded form the session.
            // This section intentionally left blank.
        } elseif( ! $this->is_preview() ) {
            // Published forms rely on the Database for the "truth" about Actions.
            $actions = Ninja_Forms()->form($this->_form_id)->get_actions();
            $this->_form_cache[ 'actions' ] = array();
            foreach( $actions as $action ){
                $action_id = $action->get_id();
                $this->_form_cache[ 'actions' ][ $action_id ] = array(
                    'id' => $action_id,
                    'settings' => $action->get_settings()
                );
            }
        } else {
            // Previews uses user option for stored data.
            $preview_data = get_user_option( 'nf_form_preview_' . $this->_form_id );
            $this->_form_cache[ 'actions' ] = $preview_data[ 'actions' ];
        }
        /* END form cache bypass. */

        // Sort Actions by Timing Order, then by Priority Order.
        usort( $this->_form_cache[ 'actions' ], array( $this, 'sort_form_actions' ) );

        /*
         * Filter Actions so that they can be pragmatically disabled by add-ons.
         *
         * ninja_forms_submission_actions
         * ninja_forms_submission_actions_preview
         */
        $this->_form_cache[ 'actions' ] = apply_filters( 'ninja_forms_submission_actions', $this->_form_cache[ 'actions' ], $this->_form_cache, $this->_form_data );
        if( $this->is_preview() ) {
            $this->_form_cache['actions'] = apply_filters('ninja_forms_submission_actions_preview', $this->_form_cache['actions'], $this->_form_cache);
        }

        // Initialize the process actions log.
        if( ! isset( $this->_data[ 'processed_actions' ] ) ) $this->_data[ 'processed_actions' ] = array();

        /*
         * Merging extra data that may have been added by fields during processing so that the values aren't lost when we enter the action loop.
         */
        $this->_data[ 'extra' ] = array_merge( $this->_data[ 'extra' ], $this->_form_data[ 'extra' ] );

        /**
         * The Action Processing Loop
         */
        foreach( $this->_form_cache[ 'actions' ] as $key => $action ){

            /** Get the action ID */
            /*
             * TODO: Refactor data structures to match.
             * Preview: Action IDs are stored as the associated array key.
             * Publish: Action IDs are stored as an array key=>value pair.
             */
            if( $this->is_preview() ){
                $action[ 'id' ] = $key;
            }

            // Duplicate the Action ID for access as a setting.
            $action[ 'settings' ][ 'id' ] = $action[ 'id' ];

            // Duplicate action ID as single variable for more readable array access.
            $action_id = $action[ 'id' ];

            // If an action has already run (ie resume submission), do not re-process.
            if( in_array( $action[ 'id' ], $this->_data[ 'processed_actions' ] ) ) continue;

            $action[ 'settings' ] = apply_filters( 'ninja_forms_run_action_settings', $action[ 'settings' ], $this->_form_id, $action[ 'id' ], $this->_form_data['settings'] );
            if( $this->is_preview() ){
                $action[ 'settings' ] = apply_filters( 'ninja_forms_run_action_settings_preview', $action[ 'settings' ], $this->_form_id, $action[ 'id' ], $this->_form_data['settings'] );
            }

            if( ! $action[ 'settings' ][ 'active' ] ) continue;

            if( ! apply_filters( 'ninja_forms_run_action_type_' . $action[ 'settings' ][ 'type' ], TRUE ) ) continue;

            $type = $action[ 'settings' ][ 'type' ];

            if( ! is_string( $type ) ) continue;

            /*
             *  test if Ninja_Forms()->actions[ $type ] is not empty
             */
            if(isset(Ninja_Forms()->actions[ $type ])) 
            { 
                $action_class = Ninja_Forms()->actions[ $type ];

                if( ! method_exists( $action_class, 'process' ) ) continue;
    
                if( $data = $action_class->process($action[ 'settings' ], $this->_form_id, $this->_data ) )
                {
                    $this->_data = apply_filters( 'ninja_forms_post_run_action_type_' . $action[ 'settings' ][ 'type' ], $data );
                }
            }

//            $this->_data[ 'actions' ][ $type ][] = $action;

            $this->maybe_halt( $action[ 'id' ] );
        }

        do_action( 'ninja_forms_after_submission', $this->_data );

        $this->_respond();
    }

    protected function validate_field( $field_settings )
    {
        $field_settings = apply_filters( 'ninja_forms_pre_validate_field_settings', $field_settings );

        if( ! is_string( $field_settings['type'] ) ) return;

        $field_class = Ninja_Forms()->fields[ $field_settings['type'] ];

        if( ! method_exists( $field_class, 'validate' ) ) return;

        if( $errors = $field_class->validate( $field_settings, $this->_form_data ) ){
            $field_id = $field_settings[ 'id' ];
            $this->_errors[ 'fields' ][ $field_id ] = $errors;
            $this->_respond();
        }
    }

    protected function process_field( $field_settings )
    {
        if( ! is_string( $field_settings['type'] ) ) return;

        $field_class = Ninja_Forms()->fields[ $field_settings['type'] ];

        if( ! method_exists( $field_class, 'process' ) ) return;

        if( $data = $field_class->process( $field_settings, $this->_form_data ) ){
            $this->_form_data = $data;
        }
    }

    protected function maybe_halt( $action_id )
    {
        if( isset( $this->_data[ 'errors' ] ) && $this->_data[ 'errors' ] ){
            $this->_respond();
        }

        if( isset( $this->_data[ 'halt' ] ) && $this->_data[ 'halt' ] ){

            Ninja_Forms()->session()->set( 'nf_processing_data', $this->_data );
            Ninja_Forms()->session()->set( 'nf_processing_form_data', $this->_form_data );
            Ninja_Forms()->session()->set( 'nf_processing_form_cache', $this->_form_cache );

            $this->_respond();
        }

        array_push( $this->_data[ 'processed_actions' ], $action_id );
    }

    protected function sort_form_actions( $a, $b )
    {
        if( is_object( $a ) ) {
            if( ! isset( Ninja_Forms()->actions[ $a->get_setting( 'type' ) ] ) ) return -1;
            $a = Ninja_Forms()->actions[ $a->get_setting( 'type' ) ];
        } else {
            if( ! isset( Ninja_Forms()->actions[ $a[ 'settings' ][ 'type' ] ] ) ) return -1;
            $a = Ninja_Forms()->actions[ $a[ 'settings' ][ 'type' ] ];
        }

        if( is_object( $b ) ) {
            if( ! isset( Ninja_Forms()->actions[ $b->get_setting( 'type' ) ] ) ) return 1;
            $b = Ninja_Forms()->actions[ $b->get_setting( 'type' ) ];
        } else {
            if( ! isset( Ninja_Forms()->actions[ $b[ 'settings' ][ 'type' ] ] ) ) return 1;
            $b = Ninja_Forms()->actions[ $b[ 'settings' ][ 'type' ] ];
        }

        if ( $a->get_timing() == $b->get_timing() ) {
            if ( $a->get_priority() == $b->get_priority() ) {
                return 0;
            }
            return ( $a->get_priority() < $b->get_priority() ) ? -1 : 1;
        }

        return ( $a->get_timing() < $b->get_timing() ) ? -1 : 1;
    }

    public function shutdown()
    {
        $error = error_get_last();
        if( $error !== NULL && in_array( $error[ 'type' ], array( E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ) ) ) {

            $this->_errors[ 'form' ][ 'last' ] = esc_html__( 'The server encountered an error during processing.', 'ninja-forms' );

            if( current_user_can( 'manage_options' ) && isset( $error[ 'message' ] ) ){
                $this->_errors[ 'form' ][ 'last_admin' ] = '<pre>' . $error[ 'message' ] . '</pre>';
            }

            $this->_errors[ 'last' ] = $error;
            Ninja_Forms()->logger()->emergency( $error[ 'message' ] );
            $this->_respond();
        }
    }

    protected function form_data_check()
    {
        if( $this->_form_data ) return;

        if( function_exists( 'json_last_error' ) // Function not supported in php5.2
            && function_exists( 'json_last_error_msg' )// Function not supported in php5.4
            && json_last_error() ){
            $this->_errors[] = json_last_error_msg();
        } else {
            $this->_errors[] = esc_html__( 'An unexpected error occurred.', 'ninja-forms' );
        }

        $this->_respond();
    }

    protected function is_preview()
    {
        if( ! isset( $this->_form_data[ 'settings' ][ 'is_preview' ] ) ) return false;
        return $this->_form_data[ 'settings' ][ 'is_preview' ];
    }

    /*
     * Overwrite method for parent class.
     */
    protected function _respond( $data = array() )
    {
        // Restore form instance ID.
        if(property_exists($this, '_form_instance_id') 
            && $this->_form_instance_id){
            $this->_data[ 'form_id' ] = $this->_form_instance_id;

            // Maybe update IDs for field errors, if there are field errors.
            if(isset($this->_errors['fields']) && $this->_errors['fields']){
                $field_errors = array();
                foreach($this->_errors['fields'] as $field_id => $error){
                    $field_errors[$field_id . '_' . $this->_instance_id] = $error;
                }
                $this->_errors['fields'] = $field_errors;
            }
        }

        // Set a content type of JSON for the purpose of previnting XSS attacks.
        header( 'Content-Type: application/json' );
        // Call the parent method.
        parent::_respond();
    }
}
