<?php
/**
 * Gravity Flow Step Approval
 *
 * @package     GravityFlow
 * @subpackage  Classes/Gravity_Flow_Step_Approval
 * @copyright   Copyright (c) 2015-2018, Steven Henty S.L.
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0
 */

if ( ! class_exists( 'GFForms' ) ) {
	die();
}

/**
 * Class Gravity_Flow_Step_Approval
 */
class Gravity_Flow_Step_Approval extends Gravity_Flow_Step {

	use Editable_Fields;

	/**
	 * The step type.
	 *
	 * @var string
	 */
	public $_step_type = 'approval';

	/**
	 * The resource slug for the REST API.
	 *
	 * @var string
	 */
	protected $_rest_base = 'approvals';

	/**
	 * Returns an array of statuses and their properties.
	 *
	 * @return array
	 */
	public function get_status_config() {
		return array(
			array(
				'status'                    => 'rejected',
				'status_label'              => __( 'Rejected', 'gravityflow' ),
				'destination_setting_label' => esc_html__( 'Next step if Rejected', 'gravityflow' ),
				'default_destination'       => 'complete',
			),
			array(
				'status'                    => 'approved',
				'status_label'              => __( 'Approved', 'gravityflow' ),
				'destination_setting_label' => __( 'Next Step if Approved', 'gravityflow' ),
				'default_destination'       => 'next',
			),
		);
	}

	/**
	 * Returns an array of quick actions to be displayed on the inbox.
	 *
	 * @return array
	 */
	public function get_actions() {
		return array(
			array(
				'key'             => 'approve',
				'label'           => __( 'Approve', 'gravityflow' ),
				'icon'            => $this->get_approve_icon(),
				'show_note_field' => in_array( $this->note_mode, array(
						'required_if_approved',
						'required_if_reverted_or_rejected',
						'required',
					)
				),
			),
			array(
				'key'             => 'reject',
				'label'           => __( 'Reject', 'gravityflow' ),
				'icon'            => $this->get_reject_icon(),
				'show_note_field' => in_array( $this->note_mode, array(
						'required_if_rejected',
						'required_if_reverted_or_rejected',
						'required',
					)
				),

			),
		);
	}

	/**
	 * Process the REST request for an entry.
	 *
	 * @since 1.7.1
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 *
	 * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response
	 *                                is already an instance, WP_HTTP_Response, otherwise
	 *                                returns a new WP_REST_Response instance.
	 */
	public function rest_callback( $request ) {
		if ( $request->get_method() !== 'POST' ) {
			return new WP_Error( 'invalid_request_method', __( 'Invalid request method' ) );
		}
		$action = $request['action'];
		$new_status = '';
		switch ( $action ) {
			case 'approve':
				$new_status = 'approved';
				break;
			case 'reject':
				$new_status = 'rejected';
		}

		if ( empty( $new_status ) ) {
			return new WP_Error( 'invalid_action', __( 'Action not supported.', 'gravityflow' ) );
		}

		$note = $request['gravityflow_note'];

		$valid_note = $this->validate_note_mode( $new_status, $note );

		if ( ! $valid_note ) {
			$response = array( 'status' => 'note_required', 'feedback' => __( 'A note is required.', 'gravityflow' ) );
			$response = rest_ensure_response( $response );
			return $response;
		}

		$assignees = $this->get_assignees();

		foreach ( $assignees as $assignee ) {
			if ( $assignee->is_current_user() ) {
				$feedback = $this->process_assignee_status( $assignee, $new_status, $this->get_form() );
				break;
			}
		}

		if ( empty( $assignee ) ) {
			return new WP_Error( 'not_supported', __( 'Action not supported.', 'gravityflow' ) );
		}

		$response = array( 'status' => 'success', 'feedback' => $feedback );

		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Indicates this step supports due date.
	 *
	 * @since 2.5
	 *
	 * @return bool
	 */
	public function supports_due_date() {
		return true;
	}

	/**
	 * Indicates this step supports expiration.
	 *
	 * @return bool
	 */
	public function supports_expiration() {
		return true;
	}

	/**
	 * Returns the step label.
	 *
	 * @return string
	 */
	public function get_label() {
		return esc_html__( 'Approval', 'gravityflow' );
	}

	/**
	 * Returns the HTML for the step icon.
	 *
	 * @return string
	 */
	public function get_icon_url() {
		return '<i class="fa fa-check" style="color:darkgreen;"></i>';
	}

	/**
	 * Returns an array of settings for this step type.
	 *
	 * @return array
	 */
	public function get_settings() {
		$settings_api = $this->get_common_settings_api();

		$settings = array(
			'title'  => esc_html__( 'Approval', 'gravityflow' ),
			'fields' => array(
				$settings_api->get_setting_assignee_type(),
				$settings_api->get_setting_assignees(),
				$settings_api->get_setting_assignee_routing(),
				array(
					'name'          => 'assignee_policy',
					'label'         => __( 'Approval Policy', 'gravityflow' ),
					'tooltip'       => __( 'Define how approvals should be processed. If all assignees must approve then the entry will require unanimous approval before the step can be completed. If the step is assigned to a role only one user in that role needs to approve.', 'gravityflow' ),
					'type'          => 'radio',
					'default_value' => 'all',
					'choices'       => array(
						array(
							'label' => __( 'Only one assignee is required to approve', 'gravityflow' ),
							'value' => 'any',
						),
						array(
							'label' => __( 'All assignees must approve', 'gravityflow' ),
							'value' => 'all',
						),
					),
				),
				$settings_api->get_setting_instructions( esc_html__( 'Instructions: please review the values in the fields below and click on the Approve or Reject button', 'gravityflow' ) ),
				$settings_api->get_setting_display_fields(),
				array(
					'name'    => 'confirmation_prompt',
					'label'   => esc_html__( 'Require Confirmation', 'gravityflow' ),
					'type'    => 'checkbox',
					'tooltip' => esc_html__( 'Activate this setting to display an additional browser prompt for the assignee to confirm before their approval/rejection is submitted.', 'gravityflow' ),
					'choices' => array(
						array(
							'label' => esc_html__( 'Enable confirmation prompt before step submission', 'gravityflow' ),
							'name'  => 'confirmation_prompt',
						),
					),
				),
			),
		);

		if ( $this->fields_have_conditional_logic( $this->get_form() ) ) {
			$settings['fields'][] = $this->get_fields_conditional_logic_settings();
		}

		$notification_tabs = $settings_api->get_setting_notification_tabs( array(
			array(
				'label'  => __( 'Assignee Email', 'gravityflow' ),
				'id'     => 'tab_assignee_notification',
				'fields' => $settings_api->get_setting_notification( array(
					'default_message' => __( 'A new entry is pending your approval. Please check your Workflow Inbox.', 'gravityflow' ),
				) ),
			),
			array(
				'label'  => __( 'Rejection Email', 'gravityflow' ),
				'id'     => 'tab_rejection_notification',
				'fields' => $settings_api->get_setting_notification( array(
					'name_prefix'      => 'rejection',
					'checkbox_label'   => __( 'Send email when the entry is rejected', 'gravityflow' ),
					'checkbox_tooltip' => __( 'Enable this setting to send an email when the entry is rejected.', 'gravityflow' ),
					'default_message'  => __( 'Entry {entry_id} has been rejected', 'gravityflow' ),
					'send_to_fields'   => true,
					'resend_field'     => false,
				) ),
			),
			array(
				'label'  => __( 'Approval Email', 'gravityflow' ),
				'id'     => 'tab_approval_notification',
				'fields' => $settings_api->get_setting_notification( array(
					'name_prefix'      => 'approval',
					'checkbox_label'   => __( 'Send email when the entry is approved', 'gravityflow' ),
					'checkbox_tooltip' => __( 'Enable this setting to send an email when the entry is approved.', 'gravityflow' ),
					'default_message'  => __( 'Entry {entry_id} has been approved', 'gravityflow' ),
					'send_to_fields'   => true,
					'resend_field'     => false,
				) ),
			),
		) );

		$user_input_step_choices = array();
		$revert_field            = array();
		$form_id                 = $this->get_form_id();
		$steps                   = gravity_flow()->get_steps( $form_id );
		foreach ( $steps as $step ) {
			if ( $step->get_type() === 'user_input' ) {
				$user_input_step_choices[] = array(
					'label' => $step->get_name(),
					'value' => $step->get_id(),
				);
			}
		}

		if ( ! empty( $user_input_step_choices ) ) {
			$revert_field = array(
				'name'     => 'revert',
				'label'    => esc_html__( 'Revert to User Input step', 'gravityflow' ),
				'type'     => 'checkbox_and_select',
				'tooltip'  => esc_html__( 'The Revert setting enables a third option in addition to Approve and Reject which allows the assignee to send the entry directly to a User Input step without changing the status. Enable this setting to show the Revert button next to the Approve and Reject buttons and specify the User Input step the entry will be sent to.', 'gravityflow' ),
				'checkbox' => array(
					'label'         => esc_html__( 'Enable', 'gravityflow' ),
					'default_value' => '0',
				),
				'select'   => array(
					'choices' => $user_input_step_choices,
				),
			);
		}

		$note_mode_setting = array(
			'name'          => 'note_mode',
			'label'         => esc_html__( 'Workflow Note', 'gravityflow' ),
			'type'          => 'select',
			'tooltip'       => esc_html__( 'The text entered in the Note box will be added to the timeline. Use this setting to select the options for the Note box.', 'gravityflow' ),
			'default_value' => 'not_required',
			'choices'       => array(
				array( 'value' => 'hidden', 'label' => esc_html__( 'Hidden', 'gravityflow' ) ),
				array( 'value' => 'not_required', 'label' => esc_html__( 'Not required', 'gravityflow' ) ),
				array( 'value' => 'required', 'label' => esc_html__( 'Always required', 'gravityflow' ) ),
				array(
					'value' => 'required_if_approved',
					'label' => esc_html__( 'Required if approved', 'gravityflow' ),
				),
				array(
					'value' => 'required_if_rejected',
					'label' => esc_html__( 'Required if rejected', 'gravityflow' ),
				),
			),
		);

		if ( ! empty( $revert_field ) ) {
			$note_mode_setting['choices'][] = array(
				'value' => 'required_if_reverted',
				'label' => esc_html__( 'Required if reverted', 'gravityflow' )
			);
			$note_mode_setting['choices'][] = array(
				'value' => 'required_if_reverted_or_rejected',
				'label' => esc_html__( 'Required if reverted or rejected', 'gravityflow' )
			);
			$settings['fields'][]           = $revert_field;

			$notification_tabs['tabs'][] = array(
				'label'  => __( 'Revert Email', 'gravityflow' ),
				'id'     => 'tab_revert_notification',
				'fields' => $settings_api->get_setting_notification( array(
					'name_prefix'      => 'revert',
					'checkbox_label'   => __( 'Send email when the entry is reverted to a user input step', 'gravityflow' ),
					'checkbox_tooltip' => __( 'Enable this setting to send an email when the entry is reverted to a user input. The assignee email for the user input step will not be sent as a result. ', 'gravityflow' ),
					'default_message'  => __( 'Entry {entry_id} has been reverted', 'gravityflow' ),
					'send_to_fields'   => true,
					'resend_field'     => false,
				) ),
			);

		}

		$settings['fields'][] = $notification_tabs;
		$settings['fields'][] = $note_mode_setting;

		$form = gravity_flow()->get_current_form();
		if ( GFCommon::has_post_field( $form['fields'] ) ) {
			$settings['fields'][] = array(
				'name'    => 'post_action_on_rejection',
				'label'   => __( 'Post Action if Rejected:', 'gravityflow' ),
				'type'    => 'select',
				'choices' => array(
					array( 'label' => '' ),
					array( 'label' => __( 'Mark Post as Draft', 'gravityflow' ), 'value' => 'draft' ),
					array( 'label' => __( 'Trash Post', 'gravityflow' ), 'value' => 'trash' ),
					array( 'label' => __( 'Delete Post', 'gravityflow' ), 'value' => 'delete' ),

				),
			);

			$settings['fields'][] = array(
				'name'    => 'post_action_on_approval',
				'label'   => __( 'Post Action if Approved:', 'gravityflow' ),
				'type'    => 'checkbox',
				'choices' => array(
					array( 'label' => __( 'Publish Post', 'gravityflow' ), 'name' => 'publish_post_on_approval' ),

				),
			);
		}

		$settings['fields'][] = array(
			'name'     => 'approved_message',
			'label'    => __( 'Approval Confirmation', 'gravityflow' ),
			'type'     => 'checkbox_and_textarea',
			'callback' => gravity_flow()->is_gravityforms_supported( '2.5-beta-1' ) ? null : array( gravity_flow(), 'legacy_settings_checkbox_and_textarea' ),
			'tooltip'  => esc_html__( 'Enable this setting to customize the confirmation message displayed when an assignee approves the entry.', 'gravityflow' ),
			'checkbox' => array(
				'label' => esc_html__( 'Display a custom confirmation message when an assignee approves', 'gravityflow' ),
				'value' => '0',
			),
			'textarea' => array(
				'use_editor'    => true,
				'default_value' => 'Entry Approved',
				'class'         => 'merge-tag-support mt-hide_all_fields mt-position-right',
			),
		);

		if ( ! empty( $revert_field ) ) {
			$settings['fields'][] = array(
				'name'     => 'reverted_message',
				'label'    => __( 'Reverted Confirmation', 'gravityflow' ),
				'type'     => 'checkbox_and_textarea',
				'callback' => gravity_flow()->is_gravityforms_supported( '2.5-beta-1' ) ? null : array( gravity_flow(), 'legacy_settings_checkbox_and_textarea' ),
				'tooltip'  => esc_html__( 'Enable this setting to customize the confirmation message displayed when an assignee reverts the entry to a user input step on this workflow.', 'gravityflow' ),
				'checkbox' => array(
					'label' => esc_html__( 'Display a custom confirmation message when an assignee reverts', 'gravityflow' ),
					'value' => '0',
				),
				'textarea' => array(
					'use_editor'    => true,
					'default_value' => 'Entry Reverted',
					'class'         => 'merge-tag-support mt-hide_all_fields mt-position-right',
				),
			);
		}

		$settings['fields'][] = array(
			'name'     => 'rejected_message',
			'label'    => __( 'Rejection Confirmation', 'gravityflow' ),
			'type'     => 'checkbox_and_textarea',
			'callback' => gravity_flow()->is_gravityforms_supported( '2.5-beta-1' ) ? null : array( gravity_flow(), 'legacy_settings_checkbox_and_textarea' ),
			'tooltip'  => esc_html__( 'Enable this setting to customize the confirmation message displayed when an assignee rejects the entry on this step.', 'gravityflow' ),
			'checkbox' => array(
				'label' => esc_html__( 'Display a custom confirmation message when an assignee rejects', 'gravityflow' ),
				'value' => '0',
			),
			'textarea' => array(
				'use_editor'    => true,
				'default_value' => 'Entry Rejected',
				'class'         => 'merge-tag-support mt-hide_all_fields mt-position-right',
			),
		);

		$settings['fields'][] = array(
			'name'     => 'processed_step_message',
			'label'    => __( 'Invalid Approval Link Message', 'gravityflow' ),
			'type'     => 'checkbox_and_textarea',
			'callback' => gravity_flow()->is_gravityforms_supported( '2.5-beta-1' ) ? null : array( gravity_flow(), 'legacy_settings_checkbox_and_textarea' ),
			'tooltip'  => esc_html__( 'Enable this setting to display a message to users who click an approve or reject link after this step has been processed.', 'gravityflow' ),
			'checkbox' => array(
				'label' => esc_html__( 'Display a custom message for the approval links when the entry is no longer on this step', 'gravityflow' ),
				'value' => '0',
			),
			'textarea' => array(
				'use_editor'    => true,
				'default_value' => 'This link is no longer valid.',
				'class'         => 'merge-tag-support mt-hide_all_fields mt-position-right',
			),
		);

		return $settings;
	}

	/**
	 * Set the assignees for this step.
	 *
	 * @return bool
	 */
	public function process() {
		return $this->assign();
	}

	/**
	 * Determines if the current step has been completed.
	 *
	 * @return bool
	 */
	public function is_complete() {
		$status = $this->evaluate_status();

		return ! in_array( $status, array( 'pending', 'queued' ) );
	}

	/**
	 * Determines the current status of the step.
	 *
	 * @return string
	 */
	public function status_evaluation() {
		$approvers   = $this->get_assignees();
		$step_status = 'approved';

		foreach ( $approvers as $approver ) {

			$approver_status = $approver->get_status();

			if ( $approver_status == 'rejected' ) {
				$step_status = 'rejected';
				break;
			}
			if ( $this->assignee_policy == 'any' ) {
				if ( $approver_status == 'approved' ) {
					$step_status = 'approved';
					break;
				} else {
					$step_status = 'pending';
				}
			} else if ( empty( $approver_status ) || $approver_status == 'pending' ) {
				$step_status = 'pending';
			}
		}

		/**
		 * Allows the step status for the approval to be customized
		 *
		 * @since 2.1-dev
		 *
		 * @param string                     $step_status   The status of the step
		 * @param Gravity_Flow_Assignee[]    $approvers     The array of Gravity_Flow_Assignee objects
		 * @param Gravity_Flow_Step          $step          The current step
		 */
		$step_status = apply_filters( 'gravityflow_step_status_evaluation_approval', $step_status, $approvers, $this );

		return $step_status;
	}

	/**
	 * Decodes and validates the supplied token.
	 *
	 * @param array $token The token properties.
	 *
	 * @return bool
	 */
	public function is_valid_token( $token ) {
		$token_json  = base64_decode( $token );
		$token_array = json_decode( $token_json, true );

		if ( empty( $token_array ) ) {
			return false;
		}

		$timestamp  = $token_array['timestamp'];
		$user_id    = $token_array['user_id'];
		$new_status = $token_array['new_status'];
		$entry_id   = $token_array['entry_id'];
		$sig        = $token_array['sig'];

		$expiration_days = apply_filters( 'gravityflow_approval_token_expiration_days', 1 );

		$i = wp_nonce_tick();

		$is_valid = false;

		for ( $n = 1; $n <= $expiration_days; $n ++ ) {
			$sig_key          = sprintf( '%s|%s|%s|%s|%s|%s', $i, $this->get_id(), $timestamp, $entry_id, $user_id, $new_status );
			$verification_sig = substr( wp_hash( $sig_key ), - 12, 10 );
			if ( hash_equals( $verification_sig, $sig ) ) {
				$is_valid = true;
				break;
			}
			$i --;
		}

		return $is_valid;
	}

	/**
	 * Handles POSTed values from the workflow detail page.
	 *
	 * @param array $form  The current form.
	 * @param array $entry The current entry.
	 *
	 * @return string|bool|WP_Error Return a success feedback message safe for page output or a WP_Error instance with an error.
	 */
	public function maybe_process_status_update( $form, $entry ) {
		$feedback        = false;
		$step_status_key = 'gravityflow_approval_new_status_step_' . $this->get_id();

		if ( isset( $_REQUEST[ $step_status_key ] ) || isset( $_GET['gflow_token'] ) || $token = gravity_flow()->decode_access_token() ) {
			if ( isset( $_POST['_wpnonce'] ) && check_admin_referer( 'gravityflow_approvals_' . $this->get_id() ) ) {
				$new_status = rgpost( $step_status_key );
				$validation = $this->validate_status_update( $new_status, $form );
				if ( is_wp_error( $validation ) ) {
					return $validation;
				}
			} else {

				$gflow_token = rgget( 'gflow_token' );
				$new_status  = rgget( 'new_status' );

				if ( ! $gflow_token ) {
					return false;
				}

				if ( $gflow_token ) {
					$token_json  = base64_decode( $gflow_token );
					$token_array = json_decode( $token_json, true );

					if ( empty( $token_array ) ) {
						return false;
					}

					$new_status = $token_array['new_status'];
					if ( empty( $new_status ) ) {
						return false;
					}
				}

				$valid_token = $this->is_valid_token( $gflow_token );

				if ( ! ( $valid_token ) ) {
					return false;
				}
			}

			$assignees = $this->get_assignees();

			foreach ( $assignees as $assignee ) {
				if ( $assignee->is_current_user() ) {
					$feedback = $this->process_assignee_status( $assignee, $new_status, $form );
					break;
				}
			}

			$entry = $this->refresh_entry();

			do_action( 'gravityflow_post_status_update_approval', $entry, $assignee, $new_status, $form );

			/**
			 * Allows the user feedback to be modified after processing the approval status update.
			 *
			 * @since 2.0.2 Added the current step
			 * @since 1.7.1
			 *
			 * @param string                $feedback   The feedback to send to the browser.
			 * @param array                 $entry      The current entry array.
			 * @param Gravity_Flow_Assignee $assignee   The assignee object.
			 * @param string                $new_status The new status
			 * @param array                 $form       The current form array.
			 * @param Gravity_Flow_Step     $step       The current step
			 */
			$feedback = apply_filters( 'gravityflow_feedback_approval', $feedback, $entry, $assignee, $new_status, $form, $this );

		}

		return $feedback;
	}

	/**
	 * Validates and performs the assignees status update.
	 *
	 * @param Gravity_Flow_Assignee $assignee   The assignee properties.
	 * @param string                $new_status The new status for this step.
	 * @param array                 $form       The current form.
	 *
	 * @return bool|string Return a success feedback message safe for page output or false.
	 */
	public function process_assignee_status( $assignee, $new_status, $form ) {
		$status_types = array( 'pending', 'approved', 'rejected', 'revert' );

		/**
		 * Allows the status types of the approval step process to be modified.
		 *
		 * @since 2.9.8
		 *
		 * @param array                 $status_types The status types the approval step will process.
		 * @param Gravity_Flow_Assignee $assignee     The assignee object.
		 * @param string                $new_status   The new status
		 * @param array                 $form         The current form array.
		 * @param Gravity_Flow_Step     $step         The current step
		 */
		$status_types = apply_filters( 'gravityflow_approval_assignee_status_types', $status_types, $assignee, $new_status, $form, $this );

		if ( ! in_array( $new_status, $status_types ) ) {
			return false;
		}

		if ( $new_status == 'revert' ) {
			return $this->process_revert_status();
		}

		/**
		 * Allows the assignee status feedback of approval step to be modified.
		 *
		 * @since 2.9.8
		 *
		 * @param bool|string           $status_feedback The status feedback for the assignee process
		 * @param Gravity_Flow_Assignee $assignee        The assignee object.
		 * @param string                $new_status      The new status
		 * @param array                 $form            The current form array.
		 * @param Gravity_Flow_Step     $step            The current step
		 */
		$custom_status_feedback = apply_filters( 'gravityflow_approval_assignee_status_feedback', false, $assignee, $new_status, $form, $this );

		if ( $custom_status_feedback ) {
			return $custom_status_feedback;
		}

		$assignee->process_status( $new_status );

		$this->add_status_update_note( $new_status, $assignee );
		$status = $this->evaluate_status();
		$this->update_step_status( $status );
		$this->refresh_entry();

		return $this->get_status_update_feedback( $new_status );
	}

	/**
	 * If the revert settings are configured end the current step and start the specified step.
	 *
	 * @return bool|string
	 */
	public function process_revert_status() {
		$feedback = false;

		if ( $this->revertEnable ) {

			$this->get_entry();

			/**
			* Allows the revert step to be modified before routing.
			*
			* @since 2.5.6
			*
			* @param string                $revert_step_id    The revert step ID from current step settings.
			* @param array                 $entry             The current entry array.
			* @param array                 $form              The current form array.
			* @param Gravity_Flow_Step     $step              The current step
			*/
			$revert_step_id = apply_filters( 'gravityflow_approval_revert_step_id', $this->revertValue, $this->get_entry(), $this->get_form(), $this );

			$step = gravity_flow()->get_step( $revert_step_id, $this->get_entry() );

			if ( $step ) {
				$this->end();

				$note = $this->get_name() . ': ' . esc_html__( 'Reverted to step', 'gravityflow' ) . ' - ' . $step->get_label();
				$this->add_note( $note . $this->maybe_add_user_note(), true );

				$this->send_revert_notification();

				//Determine whether the revert notification is set and whether the user input notification should be sent or not as a result
				if ( $this->{'revert_notification_enabled'} ) {
					add_filter( 'gravityflow_notification', array( $this, 'filter_gravityflow_notification' ), 10, 4 );
				}

				$step->start();

				$feedback = $this->reverted_messageEnable ? $this->reverted_messageValue : esc_html__( 'Reverted to step:', 'gravityflow' ) . ' ' . $step->get_label();
			}
		}

		return $feedback;
	}

	/**
	 * If applicable add a note to the current entry.
	 *
	 * @param string                $new_status The new status for the step.
	 * @param Gravity_Flow_Assignee $assignee   The step assignee.
	 */
	public function add_status_update_note( $new_status, $assignee ) {
		$note = '';

		if ( $new_status == 'approved' ) {
			$note = $this->get_name() . ': ' . __( 'Approved.', 'gravityflow' );
		} elseif ( $new_status == 'rejected' ) {
			$note = $this->get_name() . ': ' . __( 'Rejected.', 'gravityflow' );
		}

		if ( ! empty( $note ) ) {
			$this->add_note( $note . $this->maybe_add_user_note(), true );
		}
	}

	/**
	 * Get the feedback for this status update.
	 *
	 * @param string $new_status The new status for the step.
	 *
	 * @return bool|string
	 */
	public function get_status_update_feedback( $new_status ) {

		switch ( $new_status ) {
			case 'approved':
				$feedback = $this->approved_messageEnable ? $this->approved_messageValue : __( 'Entry Approved', 'gravityflow' );
				break;
			case 'rejected':
				$feedback = $this->rejected_messageEnable ? $this->rejected_messageValue : __( 'Entry Rejected', 'gravityflow' );
				break;
			default:
				$feedback = false;
		}

		return $feedback;
	}

	/**
	 * Determine if this step is valid.
	 *
	 * @param string $new_status The new status for the current step.
	 * @param array  $form       The form currently being processed.
	 *
	 * @return bool
	 */
	public function validate_status_update( $new_status, $form ) {
		$valid = $this->validate_note( $new_status, $form );

		return $this->get_validation_result( $valid, $form, $new_status );
	}

	/**
	 * Determine if the note is valid.
	 *
	 * @param string $new_status The new status for the current step.
	 * @param string $note       The submitted note.
	 *
	 * @return bool
	 */
	public function validate_note_mode( $new_status, $note ) {
		$note = trim( $note );

		$valid = true;

		switch ( $this->note_mode ) {
			case 'required':
				if ( empty( $note ) ) {
					$valid = false;
				}
				break;

			case 'required_if_approved':
				if ( $new_status == 'approved' && empty( $note ) ) {
					$valid = false;
				}
				break;

			case 'required_if_rejected':
				if ( $new_status == 'rejected' && empty( $note ) ) {
					$valid = false;
				}
				break;

			case 'required_if_reverted':
				if ( $new_status == 'revert' && empty( $note ) ) {
					$valid = false;
				}
				break;

			case 'required_if_reverted_or_rejected':
				if ( ( $new_status == 'revert' || $new_status == 'rejected' ) && empty( $note ) ) {
					$valid = false;
				}
		}

		/**
		 * Allows modification of note validity.
		 *
		 * @param bool              $valid         Indicates if the note is valid.
		 * @param string            $note          The submitted note.
		 * @param string            $new_status    The new status for the current step.
		 * @param Gravity_Flow_Step $this          The current workflow step.
		 */
		$valid = apply_filters( 'gravityflow_note_valid', $valid, $note, $new_status, $this );

		return $valid;
	}

	/**
	 * Allow the validation result to be overridden using the gravityflow_validation_approval filter.
	 *
	 * @param array  $validation_result The validation result and form currently being processed.
	 * @param string $new_status        The new status for the current step.
	 *
	 * @return array
	 */
	public function maybe_filter_validation_result( $validation_result, $new_status ) {

		return apply_filters( 'gravityflow_validation_approval', $validation_result, $this );

	}

	/**
	 * Displays content inside the Workflow metabox on the workflow detail page.
	 *
	 * @param array $form The Form array which may contain validation details.
	 * @param array $args Additional args which may affect the display.
	 */
	public function workflow_detail_box( $form, $args ) {
		$status               = esc_html__( 'Pending Approval', 'gravityflow' );
		$approve_icon         = '<i class="fa fa-check" style="color:green"></i>';
		$reject_icon          = '<i class="fa fa-times" style="color:red"></i>';
		$approval_step_status = $this->get_status();
		if ( $approval_step_status == 'approved' ) {
			$status = $approve_icon . ' ' . esc_html__( 'Approved', 'gravityflow' );
		} elseif ( $approval_step_status == 'rejected' ) {
			$status = $reject_icon . ' ' . esc_html__( 'Rejected', 'gravityflow' );
		} elseif ( $approval_step_status == 'queued' ) {
			$status = esc_html__( 'Queued', 'gravityflow' );
		}
		$display_step_status = (bool) $args['step_status'];
		if ( $display_step_status ) : ?>
			<div class="gravityflow-status-box-field gravityflow-status-box-field-step-status">
				<h4>
					<?php printf( '<span class="gravityflow-status-box-field-label">%s </span><span class="gravityflow-status-box-field-value">(%s)</span>', $this->get_name(), $status ); ?>
				</h4>
			</div>
			<div class="gravityflow-status-box-field gravityflow-status-box-field-assignees">
				<?php $this->workflow_detail_status_box_status(); ?>
			</div>
		<?php endif; ?>
		<?php $this->workflow_detail_status_box_actions( $form ); ?>

		<?php
		if ( $this->confirmation_prompt ) {
			$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min';
			wp_enqueue_script( 'gravityflow_approval', $this->get_base_url() . "/js/step-approval{$min}.js", array(), $this->_version, true );

			$messages = array(
				'approveMessage' => __( 'Are you sure you want to approve this entry?', 'gravityflow'),
				'rejectMessage'  => __( 'Are you sure you want to reject this entry?', 'gravityflow' ),
				'revertMessage'  => __( 'Are you sure you want to revert this entry?', 'gravityflow' ),
			);

			/**
			* Allows the user to modify the messages for approval/rejection confirmation.
			*
			* @since 2.5.10
			*
			* @param array             $messages The array containing approval/rejection messages.
			* @param int               $form_id  The current form id.
			* @param array             $entry    The current entry array.
			* @param Gravity_Flow_Step $step     The current step.
			*/
			$confirmation_approval = apply_filters( 'gravityflow_approval_confirm_prompt_messages', $messages, $form['id'], $this->get_entry(), $this );

			$messages['approveMessage'] = wp_kses_post( $confirmation_approval['approveMessage'] );
			$messages['rejectMessage']  = wp_kses_post( $confirmation_approval['rejectMessage'] );
			$messages['revertMessage']  = wp_kses_post( $confirmation_approval['revertMessage'] );

			wp_localize_script( 'gravityflow_approval', 'gravityflow_approval_confirmation_prompts', array(
					'approveMessage' => $messages['approveMessage'],
					'rejectMessage'  => $messages['rejectMessage'],
					'revertMessage'  => $messages['revertMessage'],
				)
			);
		}
	}

	/**
	 * Display the assignee status in the workflow detail status box.
	 */
	public function workflow_detail_status_box_status() {
		?>
		<ul>
			<?php
			$assignees = $this->get_assignees();
			foreach ( $assignees as $assignee ) {
				$assignee_status_label = $assignee->get_status_label();
				$assignee_status_li    = sprintf( '<span class="gravityflow-status-box-field-value"><li>%s</li></span>', $assignee_status_label );

				echo $assignee_status_li;
			}
			?>
		</ul>
		<?php
	}

	/**
	 * Displays the note input and step action buttons in the workflow detail status box.
	 *
	 * @param array $form The current form.
	 */
	public function workflow_detail_status_box_actions( $form ) {
		$approve_icon = $this->get_approve_icon();
		$reject_icon  = $this->get_reject_icon();
		$revert_icon  = $this->get_revert_icon();

		$assignees = $this->get_assignees();

		$can_update = false;
		foreach ( $assignees as $assignee ) {
			if ( $assignee->is_current_user() ) {
				$can_update = true;
				break;
			}
		}

		if ( $can_update ) {
			wp_nonce_field( 'gravityflow_approvals_' . $this->get_id() );

			if ( $this->note_mode !== 'hidden' ) {
				?>
				<div class="gravityflow-status-box-field gravityflow-status-box-field-note">
					<span class="gravityflow-status-box-field-label">
						<label for="gravityflow-note">
							<?php
							$note_label = esc_html__( 'Note', 'gravityflow' );
							/**
							 * Allows the label for the Note field on the approval step to be modified.
							 *
							 * @since 1.8.1
							 *
							 * @param string @note_label the label
							 * @param Gravity_Flow_Step_Approval $this The current Approval Step.
							 */
							$note_label = apply_filters( 'gravityflow_approval_note_label_workflow_detail', $note_label, $this );
							echo $note_label;
							$required_indicator = ( $this->note_mode == 'required' ) ? '*' : '';
							printf( "<span class='gfield_required'>%s</span>", $required_indicator );
							?>
						</label>
					</span>
				</div>
				<span class="gravityflow-status-box-field-value">
					<textarea id="gravityflow-note" style="width:100%;" rows="4" class="wide" name="gravityflow_note"><?php
						echo rgar( $form, 'failed_validation' ) ? esc_textarea( rgpost( 'gravityflow_note' ) ) : '';
						?></textarea>
				</span>
				<?php
				$invalid_note = ( isset( $form['workflow_note'] ) && is_array( $form['workflow_note'] ) && $form['workflow_note']['failed_validation'] );
				if ( $invalid_note ) {
					printf( "<div class='gfield_description validation_message'>%s</div>", $form['workflow_note']['validation_message'] );
				}
			}

			do_action( 'gravityflow_above_approval_buttons', $this, $form );
			?>

			<div class="gravityflow-action-buttons">
				<input type="hidden" id="gravityflow_approval_new_status_step" name="gravityflow_approval_new_status_step_<?php echo $this->get_id(); ?>">
				<button  value="approved"
						type="submit"
						class="button"
						onclick="handleApprovalStepButtonClick( this )"
				>
					<?php
					$approve_label = esc_html__( 'Approve', 'gravityflow' );

					/**
					 * Allows the 'Approve' label to be modified on the Approval step.
					 *
					 * @params string $approve_label The label to be modified.
					 * @params Gravity_Flow_Step_Approval $this The current step.
					 */
					$approve_label = apply_filters( 'gravityflow_approve_label_workflow_detail', $approve_label, $this );

					echo $approve_icon . ' ' . $approve_label;
					?>
				</button>
				<button  value="rejected"
						type="submit"
						class="button"
						onclick="handleApprovalStepButtonClick( this )"
				>
					<?php
					$reject_label = esc_html__( 'Reject', 'gravityflow' );

					/**
					 * Allows the 'Reject' label to be modified on the Approval step.
					 *
					 * @params string $reject_label The label to be modified.
					 * @params Gravity_Flow_Step_Approval $this The current step.
					 */
					$reject_label = apply_filters( 'gravityflow_reject_label_workflow_detail', $reject_label, $this );

					echo $reject_icon . ' ' . $reject_label;
					?>
				</button>
				<?php if ( $this->revertEnable ) : ?>
					<button  value="revert"
							type="submit"
							class="button"
							onclick="handleApprovalStepButtonClick( this )"
					>
						<?php
						$revert_label = esc_html__( 'Revert', 'gravityflow' );

						/**
						 * Allows the 'Revert' label to be modified on the Approval step.
						 *
						 * @params string $revert_label The label to be modified.
						 * @params Gravity_Flow_Step_Approval $this The current step.
						 */
						$revert_label = apply_filters( 'gravityflow_revert_label_workflow_detail', $revert_label, $this );

						echo $revert_icon . ' ' . $revert_label;
						?>
					</button>
					<?php
				endif;
				?>
			</div>
			<?php
		}
	}

	/**
	 * Displays content inside the Workflow metabox on the Gravity Forms Entry Detail page.
	 *
	 * @param array $form The current form.
	 */
	public function entry_detail_status_box( $form ) {
		$status = $this->evaluate_status();
		?>

		<h4 style="padding:10px;"><?php echo $this->get_name() . ': ' . $status; ?></h4>

		<div style="padding:10px;">
			<ul>
				<?php
				$assignees = $this->get_assignees();
				foreach ( $assignees as $assignee ) {
					$assignee_status_label = $assignee->get_status_label();
					$assignee_status_li    = sprintf( '<li>%s</li>', $assignee_status_label );

					echo $assignee_status_li;
				}

				?>
			</ul>
		</div>
		<?php

	}

	/**
	 * Triggers sending of the approval notification.
	 */
	public function send_approval_notification() {
		$this->maybe_send_notification( 'approval' );
	}

	/**
	 * Triggers sending of the rejection notification.
	 */
	public function send_rejection_notification() {
		$this->maybe_send_notification( 'rejection' );
	}

	/**
	 * Triggers sending of the revert notification.
	 */
	public function send_revert_notification() {
		$this->maybe_send_notification( 'revert' );
	}

	/**
	 * Provides a way for a step to process a token action before anything else. If feedback is returned it is displayed and nothing else with be rendered.
	 *
	 * @param array $action The action properties.
	 * @param array $token  The assignee token properties.
	 * @param array $form   The current form.
	 * @param array $entry  The current entry.
	 *
	 * @return bool|string|WP_Error
	 */
	public function maybe_process_token_action( $action, $token, $form, $entry ) {
		$feedback = parent::maybe_process_token_action( $action, $token, $form, $entry );

		if ( $feedback ) {
			return $feedback;
		}

		if ( ! in_array( $action, array( 'approve', 'reject', 'revert' ) ) ) {
			return false;
		}

		$entry_id = rgars( $token, 'scopes/entry_id' );
		if ( empty( $entry_id ) || $entry_id != $entry['id'] ) {
			return new WP_Error( 'incorrect_entry_id', esc_html__( 'Error: incorrect entry.', 'gravityflow' ) );
		}

		$step_id = rgars( $token, 'scopes/step_id' );
		if ( empty( $step_id ) ) {
			return new WP_Error( 'step_already_processed', esc_html__( 'Error: Step already processed.', 'gravityflow' ) );
		}

		if ( $step_id != $this->get_id() ) {
			$non_current_step = gravity_flow()->get_step( $step_id, $entry );
			if ( $non_current_step && $non_current_step->processed_step_messageEnable ) {
				return new WP_Error( 'step_already_processed', $non_current_step->processed_step_messageValue );
			} else {
				return new WP_Error( 'step_already_processed', esc_html__( 'This step has already been processed.', 'gravityflow' ) );
			}
		}

		$assignee_key = sanitize_text_field( $token['sub'] );
		$assignee     = $this->get_assignee( $assignee_key );
		$new_status   = false;
		switch ( $token['scopes']['action'] ) {
			case 'approve':
				$new_status = 'approved';
				break;
			case 'reject':
				$new_status = 'rejected';
				break;
			case 'revert':
				$new_status = 'revert';
		}
		$feedback = $this->process_assignee_status( $assignee, $new_status, $form );

		/**
		 * Allows the user feedback to be modified after processing the token action.
		 *
		 * @since 2.0.2 Added the current step
		 * @since 1.7.1
		 *
		 * @param string                $feedback   The feedback to send to the browser.
		 * @param array                 $entry      The current entry array.
		 * @param Gravity_Flow_Assignee $assignee   The assignee object.
		 * @param string                $new_status The new status
		 * @param array                 $form       The current form array.
		 * @param Gravity_Flow_Step     $step       The current step
		 */
		$feedback = apply_filters( 'gravityflow_feedback_approval_token', $feedback, $entry, $assignee, $new_status, $form, $this );

		return $feedback;
	}

	/**
	 * Triggers actions to be performed when this step ends.
	 */
	public function end() {
		$status = $this->evaluate_status();
		$entry  = $this->get_entry();
		if ( $status == 'approved' ) {
			$this->send_approval_notification();
			$this->maybe_perform_post_action( $entry, $this->publish_post_on_approval ? 'publish' : '' );
		} elseif ( $status == 'rejected' ) {
			$this->send_rejection_notification();
			$this->maybe_perform_post_action( $entry, $this->post_action_on_rejection );
		}
		if ( $status == 'approved' || $status == 'rejected' ) {
			GFAPI::send_notifications( $this->get_form(), $entry, 'workflow_approval' );
		}
		parent::end();
	}

	/**
	 * If a post exists for the entry perform the configured approval or rejection action.
	 *
	 * @param array  $entry  The current entry.
	 * @param string $action The action to perform.
	 */
	public function maybe_perform_post_action( $entry, $action ) {
		$post_id = rgar( $entry, 'post_id' );
		if ( $post_id && $action ) {
			$post = get_post( $post_id );
			if ( $post instanceof WP_Post ) {
				$result = '';
				switch ( $action ) {
					case 'publish':
					case 'draft':
						$post->post_status = $action;
						$result            = wp_update_post( $post );
						break;

					case 'trash':
						$result = wp_delete_post( $post_id );
						break;

					case 'delete':
						$result = wp_delete_post( $post_id, true );
						break;
				}

				gravity_flow()->log_debug( __METHOD__ . "() - Post: {$post_id}. Action: {$action}. Result: " . var_export( (bool) $result, 1 ) );
			}
		}
	}

	/**
	 * Returns the HTML for the approve icon.
	 *
	 * @return string
	 */
	public function get_approve_icon() {
		$approve_icon = '<i class="fa fa-check" style="color:green"></i>';
		return $approve_icon;
	}

	/**
	 * Returns the HTML for the reject icon.
	 *
	 * @return string
	 */
	public function get_reject_icon() {
		$reject_icon  = '<i class="fa fa-times" style="color:red"></i>';
		return $reject_icon;
	}

	/**
	 * Returns the HTML for the revert icon.
	 *
	 * @return string
	 */
	public function get_revert_icon() {
		$revert_icon  = '<i class="fa fa-undo" style="color:blue"></i>';
		return $revert_icon;
	}

	/**
	 * Ensure User Input assignee notification does not send if an approval revert notification exists with override
	 * selected
	 *
	 * @since 2.3.2
	 *
	 * @param string            $notification The potential notification
	 * @param array             $form         The current form array.
	 * @param array             $entry        The current entry.
	 * @param Gravity_Flow_Step $step         The current step
	 *
	 * @return bool|string
	 */
	public function filter_gravityflow_notification( $notification, $form, $entry, $step ) {
		if ( $step->get_type() == 'user_input' && $this->revertEnable == true && $step->get_id() == $this->revertValue ) {
			// Current user input step is the revert step selected in approval step settings so prevent the assignee email from being sent.
			return false;
		}

		return $notification;
	}
}

Gravity_Flow_Steps::register( new Gravity_Flow_Step_Approval() );

