File: /var/www/html/ielts-store/wp-content/plugins/woocommerce-zapier/legacy/Trigger/Base.php
<?php
namespace OM4\Zapier\Trigger;
use Exception;
use OM4\Zapier\Feed\Feed;
use OM4\Zapier\Feed\FeedFactory;
use OM4\Zapier\Logger;
use OM4\Zapier\Plugin;
use stdClass;
defined( 'ABSPATH' ) || exit;
/**
 * Represents a supported Zapier trigger.
 * A trigger is an "event" that can be acted on in Zapier.
 *
 * @deprecated 2.0.0
 */
abstract class Base {
	/**
	 * Trigger name/key.
	 * Used internally, so should only contain alphanumeric characters and underscores.
	 *
	 * @var string
	 */
	protected $trigger_key = '';
	/**
	 * User-friendly title that describes the trigger.
	 * Used in the WordPress dashboard.
	 *
	 * @var string
	 */
	protected $trigger_title = '';
	/**
	 * User-friendly description that describes the trigger.
	 * Used in the WordPress dashboard.
	 * No longer used in v1.7 and above.
	 *
	 * @var string
	 */
	protected $trigger_description = '';
	/**
	 * List of WooCommerce hooks/actions that this trigger should fire on.
	 *
	 * @var array
	 */
	protected $actions = array();
	/**
	 * Stores an array of Zapier feeds that apply to this trigger.
	 *
	 * @var Feed[]
	 */
	protected $feeds;
	/**
	 * Stores the raw data that is send to Zapier when this feed executes.
	 *
	 * @var array
	 */
	protected $data;
	/**
	 * Whether or not we are sending sample data or real live data.
	 *
	 * @var boolean
	 */
	protected $is_sample = false;
	/**
	 * The numeric sort order for this Trigger.
	 * Should be a unique integer greater than zero.
	 *
	 * This sort order is used when listing the Triggers on the Add/Edit Zapier Feed dashboard screen.
	 *
	 * @var integer
	 */
	public $sort_order;
	/**
	 * Whether or not data should be sent to Zapier asynchronously (using WP Cron)
	 *
	 * @var boolean
	 */
	protected $send_asynchronously = false;
	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	protected $logger;
	/**
	 * Constructor.
	 * Important: any child classes *must* call this constructor!
	 *
	 * @throws Exception In case `Base::sort_order` not specified.
	 */
	public function __construct() {
		$this->logger = new Logger();
		if ( is_null( $this->sort_order ) ) {
			throw new Exception( 'self::sort_order must be specified' );
		}
		foreach ( $this->actions as $action_name => $action_num_args ) {
			// When the specified action occurs, schedule it to be acted on
			// during the next page load. Intercepted by the __call() function
			// below.
			add_action( $action_name, array( $this, $action_name ), 10, $action_num_args );
			// The action name that is executed on the next page load.
			// Intercepted by the __call() function below. The number of
			// accepted arguments is increased by 1 to allow for the retrying of
			// failed attempts to contact the Zapier Webhook URL.
			add_action( "zapier_triggered_{$action_name}", array( $this, "zapier_triggered_{$action_name}" ), 10, $action_num_args + 1 );
		}
	}
	/**
	 * Executed when WooCommerce executes one of the defined hooks/actions.
	 * Should gather the necessary data, in preparation for being sent to
	 * Zapier. Important: the self->is_sample() method should
	 * be used to determine whether to use real or sample data.
	 *
	 * @param array  $args        The arguments to the hook/action.
	 * @param string $action_name The name of the WordPress action/hook.
	 *
	 * @return array|false Array of data that is to be sent to Zapier, or false on failure.
	 */
	abstract public function assemble_data( $args, $action_name );
	/**
	 * Obtain the key/slug of this trigger.
	 *
	 * @return string
	 */
	public function get_trigger_key() {
		return $this->trigger_key;
	}
	/**
	 * Obtain the title of this trigger.
	 *
	 * @return string
	 */
	public function get_trigger_title() {
		return $this->trigger_title;
	}
	/**
	 * Obtain the description of this trigger.
	 *
	 * @return string
	 */
	public function get_trigger_description() {
		return $this->trigger_description;
	}
	/**
	 * Obtain the Zapier Feeds that are configured for the specified trigger.
	 * The oldest feeds are first.
	 *
	 * NOTE: multiple calls to this function will simply return the cached result.
	 *
	 * @return Feed[] Array of Feed objects
	 */
	protected function get_feeds_for_this_trigger() {
		if ( is_null( $this->feeds ) ) {
			$this->feeds = FeedFactory::get_feeds_for_trigger( $this );
		}
		return $this->feeds;
	}
	/**
	 * Clear the cached list of feeds that use this trigger.
	 *
	 * @return void
	 */
	public function clear_feed_cache() {
		$this->feeds = null;
	}
	/**
	 * Send the data to Zapier if there are one or more feeds with this trigger.
	 *
	 * @param string $action_name Hook/action name.
	 * @param array  $arguments   Hook/action arguments.
	 *
	 * @return false|void
	 */
	public function do_send( $action_name, $arguments ) {
		if ( ! $this->has_feeds() ) {
			return false;
		}
		$this->logger->debug(
			'Assembling data. Action: %s, arguments: %s.',
			array( $action_name, json_encode( $arguments ) )
		);
		$this->data = $this->assemble_data( $arguments, $action_name );
		// Only send the data to Zapier if the assembled data wasn't false.
		if ( false !== $this->data ) {
			$result = $this->send_to_zapier( $action_name, $arguments );
			$this->logger->debug( 'Sending to Zapier result: %s', $result );
		} else {
			$this->logger->error(
				'Assembled data was false. Aborting. Action: %s, arguments: %s.',
				array( $action_name, json_encode( $arguments ) )
			);
		}
	}
	/**
	 * Whether or not this Trigger has any active Zapier Feeds.
	 *
	 * @return boolean
	 */
	public function has_feeds() {
		// Make sure there is at least one active feed configured for this trigger, otherwise there is no point continuing.
		$feeds = $this->get_feeds_for_this_trigger();
		$this->logger->debug( '%s active feed(s) found for trigger %s.', array( count( $feeds ), $this->trigger_title ) );
		if ( empty( $feeds ) ) {
			$this->logger->notice( 'No feeds. Aborting.' );
			return false;
		}
		return true;
	}
	/**
	 * Sends the assembled data to Zapier.
	 * If an error occurs, the request is retried using a truncated exponential backoff strategy
	 *
	 * @see http://en.wikipedia.org/wiki/Exponential_backoff
	 *
	 * For sample data, there is no retry mechanism for failed requests.
	 *
	 * @param string $action_name Hook/action name (needed to be able to retry failed attempts).
	 * @param array  $arguments   Hook/action arguments (needed to be able to retry failed attempts).
	 *
	 * @return bool|string true on success, error message (string) on failure
	 */
	protected function send_to_zapier( $action_name, $arguments ) {
		if ( is_null( $this->data ) || ! $this->data ) {
			return;
		}
		$data = $this->data;
		/**
		 * Override the WooCommerce data that is about to be sent to Zapier.
		 *
		 * Occurs just before the data is converted to JSON.
		 *
		 * Applies to all Zapier Triggers.
		 *
		 * Important: `is_sample()` should be used to distinguish between sample and real data being sent to Zapier.
		 *
		 * @since 1.1.0
		 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.
		 *
		 * @param array  $data        The WooCommerce data about to be sent to Zapier.
		 * @param self   $this        The Trigger instance that is causing the data to be sent to Zapier.
		 * @param string $action_name Hook/action name that initiated this data send to Zapier.
		 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
		 */
		$data = apply_filters_deprecated( 'wc_zapier_data', array( $data, $this, $action_name, $arguments ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.' );
		/**
		 * Override the WooCommerce data that is about to be sent to Zapier.
		 *
		 * Occurs just before the data is converted to JSON.
		 *
		 * The dynamic portion of the filter name, $this->trigger_key,
		 * refers to the unique key of the trigger being executed.
		 *
		 * This filter allows you to override data for a specific Zapier trigger.
		 *
		 * Important: self->is_sample() should be used to distinguish between sample and real data being sent to Zapier.
		 *
		 * @since 1.1.0
		 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.
		 *
		 * @param array  $data        The WooCommerce data about to be sent to Zapier.
		 * @param self   $this        The Trigger instance that is causing the data to be sent to Zapier.
		 * @param string $action_name Hook/action name that initiated this data send to Zapier.
		 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
		 */
		$data = apply_filters_deprecated( "wc_zapier_data_{$this->trigger_key}", array( $data, $this, $action_name, $arguments ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.' );
		$json_data = json_encode( $data );
		/**
		 * Override the JSON data that is about to be sent to Zapier.
		 *
		 * Applies to all Zapier Triggers.
		 *
		 * Important: $this->is_sample() should be used to distinguish between sample and real data being sent to Zapier.
		 *
		 * @since 1.1.0
		 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.
		 *
		 * @param string $json_data   The JSON-encoded data about to be sent to Zapier.
		 * @param self   $this        The Trigger instance that is causing the data to be sent to Zapier.
		 * @param string $action_name Hook/action name that initiated this data send to Zapier.
		 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
		 */
		$json_data = apply_filters_deprecated( 'wc_zapier_data_json', array( $json_data, $this, $action_name, $arguments ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.' );
		/**
		 * Override the JSON data that is about to be sent to Zapier.
		 *
		 * The dynamic portion of the filter name, $this->trigger_key,
		 * refers to the unique key of the trigger being executed.
		 *
		 * This filter allows you to override data for a specific Zapier trigger.
		 *
		 * Important: $this->is_sample() should be used to distinguish between sample and real data being sent to Zapier.
		 *
		 * @since 1.1.0
		 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.
		 *
		 * @param string $json_data   The JSON-encoded data about to be sent to Zapier.
		 * @param self   $this        The Trigger instance that is causing the data to be sent to Zapier.
		 * @param string $action_name Hook/action name that initiated this data send to Zapier.
		 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
		 */
		$json_data = apply_filters_deprecated( "wc_zapier_data_json_{$this->trigger_key}", array( $json_data, $this, $action_name, $arguments ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API endpoint data overrides.' );
		foreach ( $this->get_feeds_for_this_trigger() as $feed ) {
			$args = array(
				'ssl'     => true,
				// Use a 10 second timeout instead of 5 seconds.
				'timeout' => 10,
				'body'    => $json_data,
				'headers' => array(
					// We're sending JSON data to Zapier.
					'Content-Type' => 'application/json',
				),
			);
			$webhook_url = $feed->webhook_url();
			if ( $this->is_sample() ) {
				// Testing the webhook - we're not sending real data.
				// Send X-Hook-Test header as per
				// https://zapier.com/developer/reference/#static-webhooks and
				// https://zapier.com/support/questions/1125/validating-urls/
				// This will never trigger an action for real
				// Zapier will just cache the payload in their UI.
				$args['headers']['X-Hook-Test'] = 'true';
			}
			$num_attempts = 0;
			if ( ! $this->is_sample() ) {
				// For real data, keep track of the number of retries.
				if ( count( $arguments ) === $this->actions[ $action_name ] ) {
					// No num_retries parameter specified.
					$num_attempts = 0;
				} else {
					// We've retried at least once.
					$num_attempts = array_pop( $arguments );
				}
				$num_attempts++;
			}
			$sampletext = $this->is_sample() ? 'Sample ' : '';
			$this->logger->debug( ' Attempting to send %sdata to Zapier Feed:', $sampletext );
			$this->logger->debug( '  - Feed ID: %s', $feed->id() );
			$this->logger->debug( '  - Feed Title: %s', $feed->title() );
			$this->logger->debug( '  - Trigger: %s', $this->get_trigger_title() );
			$this->logger->debug( '  - Webhook URL: %s', $webhook_url );
			// Logs the data partially.
			$this->logger->debug( '  - %sJSON Data: %s...', array( $sampletext, substr( $json_data, 0, 72 ) ) );
			$result = wp_remote_post( $webhook_url, $args );
			// Was there was an error communicating with the Zapier webhook?
			if ( 200 !== wp_remote_retrieve_response_code( $result ) ) {
				$error_message = 'Unknown error';
				if ( is_wp_error( $result ) ) {
					$error_message = $result->get_error_messages();
					$error_message = implode( ', ', $error_message );
				} else {
					// Non HTTP 200 response.
					// Translators: %1$d: HTTP response code. %2$s: HTTP response message.
					$error_message = sprintf( __( 'HTTP %1$d (%2$s)', 'woocommerce-zapier' ), wp_remote_retrieve_response_code( $result ), esc_html( wp_remote_retrieve_response_message( $result ) ) );
				}
				$this->logger->error( '%s for URL %s', array( $error_message, $webhook_url ) );
				$this->logger->debug( "args: %s\nresponse: %s\n", array( json_encode( $args ), json_encode( $result ) ) );
				if ( $this->is_sample() ) {
					// When sending sample data, don't automatically retry.
					return $error_message;
				}
				$arguments['num_attempts'] = $num_attempts;
				// Retry the request at a later date. Use a Truncated
				// exponential backoff strategy:
				// http://en.wikipedia.org/wiki/Exponential_backoff
				// (with a maximum retry retry time of 1 hour).
				$retry_seconds = min( (int) pow( 2, $arguments['num_attempts'] ), 3600 );
				$this->logger->warning(
					'Attempt #%s failed. Scheduling retry to occur in %s second(s) from now.',
					array( $num_attempts, $retry_seconds )
				);
				$result = $this->schedule_event( $action_name, $arguments, $retry_seconds );
				if ( false === $result ) {
					$this->logger->error( '%s::schedule_event() returned false when rescheduling.', get_called_class() );
				}
				return $error_message;
			}
			// The request was a success.
			$this->logger->debug( 'Success - HTTP 200 response code.' );
			$this->logger->debug( 'Zapier Response: %s', wp_remote_retrieve_body( $result ) );
			if ( ! $this->is_sample() ) {
				// Only do this for real data.
				$this->data_sent_to_feed( $feed, $result, $action_name, $arguments, $num_attempts );
			}
			/**
			 * After data is sent to Zapier successfully.
			 * Important: `is_sample()` should be used to distinguish between sample and real data being sent to Zapier.
			 *
			 * @since 1.6.2
			 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API based Webhooks.
			 *
			 * @param string $json_data   The JSON-encoded data that was sent to Zapier.
			 * @param self   $this        The Trigger instance that caused the data to be sent to Zapier.
			 * @param string $action_name Hook/action name that initiated this data send to Zapier.
			 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
			 */
			wc_do_deprecated_action( 'wc_zapier_data_sent_to_zapier_successfully', array( $json_data, $this, $action_name, $arguments ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API based Webhooks.' );
		}
		return true;
	}
	/**
	 * Executed every time data is sent to a Zapier feed.
	 * Not executed when sample data is sent.
	 *
	 * @param Feed   $feed         Feed data.
	 * @param array  $result       Response from the wp_remote_post() call.
	 * @param string $action_name  Hook/action name (needed to be able to retry failed attempts).
	 * @param array  $arguments    Hook/action arguments (needed to be able to retry failed attempts).
	 * @param int    $num_attempts The number of attempts it took to successfully send the data to Zapier.
	 */
	protected function data_sent_to_feed( Feed $feed, $result, $action_name, $arguments, $num_attempts = 0 ) {
	}
	/**
	 * Executed whenever one of the trigger's supported hooks/actions is called.
	 * Called initially when a supported hook/filter is called, in which case we
	 * schedule the data to be sent to Zapier on the next page load.
	 *
	 * Called again on the next page load (when WordPress cron is executing), in which case
	 * the data is assembled and send to Zapier.
	 *
	 * @param string $action_name Hook/action name.
	 * @param array  $arguments   Hook/action arguments.
	 *
	 * @return boolean
	 */
	public function __call( $action_name, array $arguments ) {
		$pos = $this->send_asynchronously() ? strpos( $action_name, 'zapier_triggered_' ) : 0;
		$this->logger->debug( '%s::__call() : %s', array( get_called_class(), $this->trigger_title ) );
		$this->logger->debug( '  - Action_name: %s', $action_name );
		$this->logger->debug( '  - Arguments: %s', json_encode( $arguments ) );
		if ( false === $pos ) {
			// One of the specified actions has just been triggered
			// Make sure it is an action/hook that we support and expect.
			if ( ! array_key_exists( $action_name, $this->actions ) ) {
				return;
			}
			if ( ! $this->has_feeds() ) {
				return;
			}
			// Rather than acting on this immediately, schedule an event to run on the next page load.
			// This also makes the Zapier API calls (somewhat) asynchronous, and allows us to retry if it fails.
			$result = $this->schedule_event( $action_name, $arguments );
			$this->logger->debug( 'Scheduled the asynchronous event to occur via wp-cron:' );
			$this->logger->debug( '  - Result: %s', $result );
			$this->logger->debug( '  - Action_name: %s', $action_name );
			$this->logger->debug( '  - Arguments: %s', json_encode( $arguments ) );
			if ( false === $result ) {
				$this->logger->warning( '%s::schedule_event() returned false when scheduling.', get_called_class() );
			}
		} elseif ( 0 === $pos ) {
			if ( defined( 'DOING_CRON' ) || function_exists( '_get_cron_lock' ) ) {
				$this->logger->debug( 'This is a cron request' );
			} else {
				$this->logger->debug( "This isn't a cron request" );
			}
			if ( ! $this->send_asynchronously() ) {
				// Sending synchronously (immediately).
				$this->logger->debug( 'Adding the task to the queue...' );
				$result = Plugin::$queue->add_to_queue( $this, $action_name, $arguments );
				return true;
			}
			if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
				$this->logger->debug( '  - URL: %s%s', array( sanitize_text_field( wp_unslash( $_SERVER[ HTTP_HOST ] ) ), sanitize_text_field( wp_unslash( $_SERVER[ REQUEST_URI ] ) ) ) );
			}
			$action_name_to_trigger = str_replace( 'zapier_triggered_', '', $action_name );
			$this->logger->debug( '  - Action to trigger: %s', $action_name_to_trigger );
			$this->logger->debug( '  - Arguments: %s', json_encode( $arguments ) );
			$this->do_send( $action_name_to_trigger, $arguments );
		} else {
			// Silently ignore this request.
			$this->logger->notice( "Action_name doesn't seem to be supported: %s", $action_name );
		}
	}
	/**
	 * Send some sample data to the specified Zapier Feed.
	 *
	 * This is done so that Zapier knows what data field to expect.
	 *
	 * @param Feed $feed The feed to send sample data to.
	 *
	 * @return true|string True on success, or an error message (string) on failure.
	 */
	public function send_sample_data_payload( Feed $feed ) {
		$this->is_sample = true;
		$this->feeds     = array( $feed );
		$this->data = $this->assemble_data( array(), 'test' );
		$result     = $this->send_to_zapier( 'test', array() );
		$this->is_sample = false;
		$this->feeds     = null;
		return $result;
	}
	/**
	 * Schedules a single event to be executed on the next page load via WordPress' cron system.
	 *
	 * @param string  $action_name                Hook/action name.
	 * @param array   $arguments                  Hook/action arguments.
	 * @param integer $number_of_seconds_from_now The Number of sends in the future to schedule the event for.
	 *
	 * @return boolean
	 */
	protected function schedule_event( $action_name, $arguments, $number_of_seconds_from_now = 5 ) {
		$result = wp_schedule_single_event( time() + $number_of_seconds_from_now, "zapier_triggered_{$action_name}", $arguments );
		if ( is_null( $result ) ) {
			// wp_schedule_single_event() returns null on success, so convert it to true.
			$result = true;
		}
		/**
		 * After data is scheduled to be sent to Zapier (asynchronously via WP-cron).
		 *
		 * @since 1.6.3
		 * @deprecated 2.0.0 Legacy Zapier Feeds should be replaced with REST API based Webhooks.
		 *
		 * @param string $action_name Hook/action name that initiated this data send to Zapier.
		 * @param array  $arguments   Arguments for the hook/action that initiated this data send to Zapier.
		 * @param self   $this        The Trigger instance that caused the data to be sent to Zapier.
		 * @param bool   $result      The result of the wp_schedule_single_event() call (added in WC Zapier v1.6.10).
		 */
		wc_do_deprecated_action( 'wc_zapier_scheduled_event', array( $action_name, $arguments, $this, $result ), '2.0.0', false, 'Legacy Zapier Feeds should be replaced with REST API based Webhooks.' );
		return $result;
	}
	/**
	 * Whether or not we are sending sample data to Zapier.
	 *
	 * @return bool true if sending sample data, false if sending real live data.
	 */
	public function is_sample() {
		return $this->is_sample;
	}
	/**
	 * Given a stdClass object, empty out the values for all properties.
	 *
	 * @param stdClass $object The object to empty.
	 *
	 * @return stdClass
	 */
	protected function create_empty_object_recursive( stdClass $object ) {
		foreach ( $object as $key => $value ) {
			if ( is_array( $value ) ) {
				foreach ( $value as $array_key => $array_item ) {
					$object->{$key}[ $array_key ] = $this->create_empty_object_recursive( $array_item );
				}
			} elseif ( is_string( $value ) ) {
				$object->{$key} = '';
			} elseif ( is_integer( $value ) ) {
				$object->{$key} = '';
			} elseif ( is_bool( $value ) ) {
				$object->{$key} = '';
			}
		}
		return $object;
	}
	/**
	 * Send sample data to all active feeds that use this trigger event.
	 */
	public function send_sample_data_to_active_feeds_using_this_trigger() {
		foreach ( FeedFactory::get_feeds_for_trigger( $this ) as $feed ) {
			$feed->trigger()->send_sample_data_payload( $feed );
		}
	}
	/**
	 * Whether or not the trigger event should send data to Zapier.
	 * Defaults to true, but allows specific triggers (sub-classes) to
	 * optionally prevent the data send from being scheduled. A trigger can
	 * override this function when/if required.
	 *
	 * @param string $action_name Hook/action name.
	 * @param array  $args        Hook/action arguments.
	 *
	 * @return bool True if the event should be scheduled, false if not
	 */
	protected function should_schedule_event( $action_name, $args ) {
		return true;
	}
	/**
	 * Whether or not to send data to Zapier asynchronously (using WP-Cron).
	 *
	 * @return bool
	 */
	public function send_asynchronously() {
		/**
		 * Override whether or not to send data to Zapier asynchronously (using WP-Cron).
		 *
		 * @since 1.6.0
		 * @deprecated 1.7.0
		 *
		 * @param boolean $send_asynchronously True to send via WP-cron, false to send immediately.
		 * @param self    $this                The Trigger instance that is causing the data to be sent to Zapier.
		 */
		$async = apply_filters( 'wc_zapier_send_asynchronously', $this->send_asynchronously, $this );
		if ( $async ) {
			_deprecated_hook( 'wc_zapier_send_asynchronously', '1.7', null, esc_html__( 'Asynchronous sending is no longer supported and will be removed in a future version', 'woocommerce-zapier' ) );
		}
		return $async;
	}
}