add_delayed_payment_support()` in `init()`. * * @since 2.7.14 Was a dynamic property in earlier versions. * * @var array { * @type string $option_label The label to displayed for the add-ons delay checkbox, in the Post Payment Actions section of the payment add-ons feed configuration page. * } */ public $delayed_payment_integration = array(); /** * @var string Version number of the Add-On Framework */ private $_feed_version = '0.14'; private $_feed_settings_fields = array(); private $_current_feed_id = false; /** * @since 2.4 * @var array Feeds w/ conditional logic that impacts frontend form behavior. */ private static $_frontend_feeds = array(); /** * @since 2.4.23 * * @var array Tables where table error has been rendered. */ private $_table_error_rendered = array(); /** * Gets all active, registered feed add-ons. * * @since 2.9.2 * * @return (GFFeedAddOn|GFPaymentAddOn)[] */ public static function get_registered_feed_addons() { $addons = GFAddOn::get_registered_addons( true, true ); return array_filter( $addons, function ( $addon ) { return $addon instanceof GFFeedAddOn; } ); } /** * Attaches any filters or actions needed to bootstrap the addon. * * @since 2.5.2 */ public function bootstrap() { if ( ! $this instanceof GFPaymentAddOn ) { if ( ! function_exists( 'gf_feed_processor' ) ) { require_once GF_PLUGIN_DIR_PATH . 'includes/addon/class-gf-feed-processor.php'; } gf_feed_processor( $this ); } parent::bootstrap(); if ( $this->is_feed_edit_page() ) { add_action( 'admin_init', array( $this, 'feed_settings_init' ), 20 ); } } /** * Plugin starting point. Handles hooks and loading of language files. */ public function init() { parent::init(); add_filter( 'gform_entry_post_save', array( $this, 'maybe_process_feed' ), 10, 2 ); add_action( 'gform_after_delete_form', array( $this, 'delete_feeds' ) ); add_action( 'gform_update_status', array( $this, 'process_feed_when_unspammed' ), 10, 3 ); // Register GFFrontendFeeds. if ( $this->_supports_frontend_feeds && ! has_action( 'gform_register_init_scripts', array( __class__, 'register_frontend_feeds_init_script' ) ) ) { // Use priority 20 so other add-ons that support frontend feeds can all load their scripts first. add_action( 'gform_register_init_scripts', array( __class__, 'register_frontend_feeds_init_script' ), 20 ); } } /** * Override this function to add AJAX hooks or to add initialization code when an AJAX request is being performed */ public function init_ajax() { parent::init_ajax(); add_action( "wp_ajax_gf_feed_is_active_{$this->get_slug()}", array( $this, 'ajax_toggle_is_active' ) ); add_action( 'wp_ajax_gf_save_feed_order', array( $this, 'ajax_save_feed_order' ) ); } /** * Override this function to add initialization code (i.e. hooks) for the admin site (WP dashboard) */ public function init_admin() { parent::init_admin(); add_filter( 'gform_notification_events', array( $this, 'notification_events' ), 10, 2 ); add_filter( 'gform_notes_avatar', array( $this, 'notes_avatar' ), 10, 2 ); add_action( 'gform_post_form_duplicated', array( $this, 'post_form_duplicated' ), 10, 2 ); } /** * Performs upgrade tasks when the version of the Add-On changes. To add additional upgrade tasks, override the upgrade() function, which will only get executed when the plugin version has changed. */ public function setup() { // upgrading Feed Add-On base class $installed_version = get_option( 'gravityformsaddon_feed-base_version' ); if ( $installed_version != $this->_feed_version ) { $this->upgrade_base( $installed_version ); update_option( 'gravityformsaddon_feed-base_version', $this->_feed_version ); } parent::setup(); } private function upgrade_base( $previous_version ) { global $wpdb; require_once( ABSPATH . '/wp-admin/includes/upgrade.php' ); if ( ! empty( $wpdb->charset ) ) { $charset_collate = "DEFAULT CHARACTER SET $wpdb->charset"; } if ( ! empty( $wpdb->collate ) ) { $charset_collate .= " COLLATE $wpdb->collate"; } $sql = "CREATE TABLE {$wpdb->prefix}gf_addon_feed ( id mediumint(8) unsigned not null auto_increment, form_id mediumint(8) unsigned not null, is_active tinyint(1) not null default 1, feed_order mediumint(8) unsigned not null default 0, meta longtext, addon_slug varchar(50), event_type varchar(20), PRIMARY KEY (id), KEY addon_form (addon_slug,form_id) ) $charset_collate;"; gf_upgrade()->dbDelta( $sql ); } /** * Gets called when Gravity Forms upgrade process is completed. This function is intended to be used internally, override the upgrade() function to execute database update scripts. * @param $db_version - Current Gravity Forms database version * @param $previous_db_version - Previous Gravity Forms database version * @param $force_upgrade - True if this is a request to force an upgrade. False if this is a standard upgrade (due to version change) */ public function post_gravityforms_upgrade( $db_version, $previous_db_version, $force_upgrade ) { // Forcing Upgrade if ( $force_upgrade ) { $installed_version = get_option( 'gravityformsaddon_feed-base_version' ); $this->upgrade_base( $installed_version ); update_option( 'gravityformsaddon_feed-base_version', $this->_feed_version ); } parent::post_gravityforms_upgrade( $db_version, $previous_db_version, $force_upgrade ); } public function scripts() { $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $scripts = array( array( 'handle' => 'gform_form_admin', 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ) ), ), array( 'handle' => 'gform_gravityforms', 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ) ), ), array( 'handle' => 'gform_forms', 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ) ), ), array( 'handle' => 'json2', 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ) ), ), array( 'handle' => 'gform_placeholder', 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ), 'field_types' => array( 'feed_condition' ), ), ) ), ); if ( $this->_supports_feed_ordering ) { $scripts[] = array( 'handle' => 'gaddon_feedorder', 'src' => $this->get_gfaddon_base_url() . "/js/gaddon_feedorder{$min}.js", 'version' => GFCommon::$version, 'deps' => array( 'jquery', 'jquery-ui-sortable' ), 'in_footer' => false, 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ), ), ); } if( $this->_supports_frontend_feeds ) { $scripts[] = array( 'handle' => 'gaddon_frontend', 'src' => $this->get_gfaddon_base_url() . "/js/gaddon_frontend{$min}.js", 'deps' => array( 'jquery', 'gform_conditional_logic' ), 'version' => GFCommon::$version, 'enqueue' => array( array( $this, 'has_frontend_feeds' ) ), ); } return array_merge( parent::scripts(), $scripts ); } /** * Deletes the feeds and clears queued background tasks on uninstall. * * @since unknown * @since 2.9.25 Updated to clear queued background tasks. * * @return void */ public function uninstall() { global $wpdb; $sql = $wpdb->prepare( "DELETE FROM {$wpdb->prefix}gf_addon_feed WHERE addon_slug=%s", $this->get_slug() ); $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared if ( ! $this instanceof GFPaymentAddOn ) { gf_feed_processor( $this )->uninstall(); } } //-------- Front-end methods --------------------------- public function set_bypass_feed_delay( $bypass ) { $this->_bypass_feed_delay = (bool) $bypass; } /** * Determines what feeds need to be processed for the provided entry. * * @since 1.7.7 * @since 2.9.4 Updated to save the processing status for each feed of compatible add-ons. * * @access public * * @param array $entry The Entry Object currently being processed. * @param array $form The Form Object currently being processed. * * @return array $entry */ public function maybe_process_feed( $entry, $form ) { $entry_id = (int) rgar( $entry, 'id' ); if ( 'spam' === rgar( $entry, 'status' ) ) { $this->log_debug( __METHOD__ . "(): Entry #{$entry_id} is marked as spam; not processing feeds." ); return $entry; } $this->log_debug( __METHOD__ . "(): Checking for feeds to process for entry #{$entry_id}." ); $form_id = (int) rgar( $form, 'id' ); $feeds = false; // If this is a single submission feed, get the first feed. Otherwise, get all feeds. if ( $this->_single_feed_submission ) { $feed = $this->get_single_submission_feed( $entry, $form ); if ( $feed ) { $feeds = array( $feed ); } } else { $feeds = $this->get_feeds( $form_id ); } // Run filters before processing feeds. $feeds = $this->pre_process_feeds( $feeds, $entry, $form ); // If there are no feeds to process, return. if ( empty( $feeds ) ) { $this->log_debug( __METHOD__ . "(): No feeds to process for entry #{$entry_id}." ); return $entry; } // Determine if feed processing needs to be delayed. $is_delayed = $this->maybe_delay_feed( $entry, $form ); // Initialize array of feeds that have been processed. $processed_feeds = array(); $background_processor = gf_feed_processor( $this ); // Loop through feeds. foreach ( $feeds as $feed ) { // Get the feed name. $feed_name = $this->get_feed_name( $feed ); $feed_id = (int) rgar( $feed, 'id' ); // If this feed is inactive, log that it's not being processed and skip it. if ( ! $feed['is_active'] ) { $this->log_debug( __METHOD__ . "(): Feed is inactive, not processing feed (#{$feed_id} - {$feed_name}) for entry #{$entry_id}." ); continue; } if ( ! $this->can_process_feed( $feed, $entry, $form ) ) { $this->log_debug( __METHOD__ . "(): Feed can't be re-processed. Not processing feed (#{$feed_id} - {$feed_name}) for entry #{$entry_id}." ); continue; } // If this feed's condition is not met, log that it's not being processed and skip it. if ( ! $this->is_feed_condition_met( $feed, $form, $entry ) ) { $this->log_debug( __METHOD__ . "(): Feed condition not met, not processing feed (#{$feed_id} - {$feed_name}) for entry #{$entry_id}." ); continue; } // process feed if not delayed if ( ! $is_delayed ) { // If asynchronous feed processing is enabled, add it to the processing queue. if ( $this->is_asynchronous( $feed, $entry, $form ) ) { // Log that feed processing is being delayed. $this->log_debug( __METHOD__ . "(): Adding feed (#{$feed_id} - {$feed_name}) to the processing queue for entry #{$entry_id}." ); // Add feed to processing queue. $background_processor->push_to_queue( array( 'addon' => get_class( $this ), 'feed' => $feed, 'entry_id' => $entry_id, 'form_id' => $form_id, ) ); $this->delay_feed( $feed, $entry, $form ); } else { // All requirements are met; process feed. $this->log_debug( __METHOD__ . "(): Starting to process feed (#{$feed_id} - {$feed_name}) for entry #{$entry_id}." ); $result = $this->process_feed( $feed, $entry, $form ); $this->save_entry_feed_status( $result, $entry_id, $feed_id, $form_id ); // If returned value from the process feed call is an array containing an id, set the entry to its value. if ( (int) rgar( $result, 'id' ) === $entry_id ) { $entry = $result; } $this->post_process_feed( $feed, $entry, $form ); $this->fulfill_entry( $entry_id, $form_id ); // Adding this feed to the list of processed feeds $processed_feeds[] = $feed_id; } } else { // Log that feed processing is being delayed. $this->log_debug( __METHOD__ . "(): Feed processing is delayed, not processing feed (#{$feed_id} - {$feed_name}) for entry #{$entry_id}." ); // Delay feed. $this->delay_feed( $feed, $entry, $form ); } } // If any feeds were processed, save the processed feed IDs. if ( ! empty( $processed_feeds ) ) { GFAPI::update_processed_feeds_meta( $entry_id, $this->get_slug(), $processed_feeds, $form_id ); } $background_processor->save()->dispatch_on_shutdown(); // Return the entry object. return $entry; } /** * Retrieves the name of the given feed. * * @since 2.9.9 * * @param array $feed The feed. * @param string $key Optional. The key used to store the name. * * @return string */ public function get_feed_name( $feed, $key = '' ) { return GFAPI::get_feed_name( $feed, $key ); } /** * Saves the status of the given feed to the "feed_{$feed_id}_status" entry meta. * * @since 2.9.4 * * @param null|bool|array|WP_Error|Exception $result The value returned by `process_feed()`. * @param int $entry_id The ID of the entry the feed was processed for. * @param int $feed_id The ID of the feed that was processed. * @param int $form_id The ID of the form the entry and feed belong to. * * @return void */ public function save_entry_feed_status( $result, $entry_id, $feed_id, $form_id ) { if ( is_null( $result ) ) { return; } $status = array( 'timestamp' => time(), 'status' => 'failed', 'code' => '', 'message' => '', 'data' => '', ); if ( $result === true || is_array( $result ) ) { $status['status'] = 'success'; } elseif ( $result instanceof Exception ) { $status['code'] = $result->getCode(); $status['message'] = $result->getMessage(); } elseif ( is_wp_error( $result ) ) { $status['code'] = $result->get_error_code(); $status['message'] = $result->get_error_message(); $status['data'] = $result->get_error_data(); } GFAPI::update_entry_feed_status( $entry_id, $feed_id, $status, $form_id ); } /** * Triggers the post_process_feed hooks. * * @since 2.9.4 * * @param array $feed The feed which was processed. * @param array $entry The entry the feed was processed for. * @param array $form The form the entry and feed belong to. * * @return void */ public function post_process_feed( $feed, $entry, $form ) { $has_action = array(); if ( has_action( 'gform_post_process_feed' ) ) { $has_action[] = 'gform_post_process_feed'; } $gform_slug_post_process_feed = "gform_{$this->get_slug()}_post_process_feed"; if ( has_action( $gform_slug_post_process_feed ) ) { $has_action[] = $gform_slug_post_process_feed; } if ( empty( $has_action ) ) { return; } $addon = $this; $this->log_debug( __METHOD__ . sprintf( '(): Executing functions hooked to %s for feed #%d and entry #%d.', implode( ' and ', $has_action ), rgar( $feed, 'id' ), rgar( $entry, 'id' ) ) ); /** * Perform a custom action when a feed has been processed. * * @since 2.0 * * @param array $feed The feed which was processed. * @param array $entry The current entry object, which may have been modified by the processed feed. * @param array $form The current form object. * @param GFFeedAddOn $addon The current instance of the add-on. */ do_action( 'gform_post_process_feed', $feed, $entry, $form, $addon ); do_action( $gform_slug_post_process_feed, $feed, $entry, $form, $addon ); } /** * Sets the "{slug}_is_fulfilled" entry meta. * * @since 2.9.4 * * @param int $entry_id The entry ID. * @param int $form_id The form ID. * * @return void */ public function fulfill_entry( $entry_id, $form_id ) { if ( gform_update_meta( $entry_id, "{$this->get_slug()}_is_fulfilled", true, $form_id ) ) { $this->log_debug( __METHOD__ . '(): Entry #' . $entry_id . ' marked as fulfilled.' ); } } /** * Determines if feed processing is delayed by another add-on. * * Also enables use of the gform_is_delayed_pre_process_feed filter. * * @param array $entry The Entry Object currently being processed. * @param array $form The Form Object currently being processed. * * @return bool */ public function maybe_delay_feed( $entry, $form ) { if ( $this->_bypass_feed_delay || $this instanceof GFPaymentAddOn ) { return false; } $is_delayed = false; $slug = $this->get_slug(); /** * Allow feed processing to be delayed. * * @param bool $is_delayed Is feed processing delayed? * @param array $form The Form Object currently being processed. * @param array $entry The Entry Object currently being processed. * @param string $slug The Add-On slug e.g. gravityformsmailchimp */ $is_delayed = apply_filters( 'gform_is_delayed_pre_process_feed', $is_delayed, $form, $entry, $slug ); $is_delayed = apply_filters( 'gform_is_delayed_pre_process_feed_' . $form['id'], $is_delayed, $form, $entry, $slug ); return $is_delayed; } /** * Retrieves the delay setting for the current add-on from the payment feed. * * @param array $payment_feed The payment feed which is being used to process the current submission. * * @return bool|null */ public function is_delayed( $payment_feed ) { $delay = rgar( $payment_feed['meta'], 'delay_' . $this->get_slug() ); return $delay; } /** * Determines if feed processing should happen asynchronously. * * @since 2.2 * @access public * * @param array $feed The Feed Object currently being processed. * @param array $form The Form Object currently being processed. * @param array $entry The Entry Object currently being processed. * * @return bool */ public function is_asynchronous( $feed, $entry, $form ) { if ( $this->_bypass_feed_delay ) { return false; } /** * Allow feed to be processed asynchronously. * * @since 2.2 * * @param bool $is_asynchronous Is feed being processed asynchronously? * @param array $feed The Feed Object currently being processed. * @param array $entry The Entry Object currently being processed. * @param array $form The Form Object currently being processed. */ $is_asynchronous = gf_apply_filters( array( 'gform_is_feed_asynchronous', $form['id'], $feed['id'] ), $this->_async_feed_processing, $feed, $entry, $form ); return $is_asynchronous; } /** * Determines if the add-on supports processing feeds multiple times for the same entry (e.g. by the async processor). * * @since 2.9.2 * * @param array $feed The feed to be processed * @param array $entry The entry being processed. * @param array $form The form that the entry belongs to * * @return bool */ public function is_reprocessing_supported( $feed, $entry, $form ) { return $this->_supports_feed_reprocessing; } /** * Determines if the feed can be processed based on the contents of the processed feeds entry meta. * * @since 2.9.19 * * @param array $feed The feed being processing. * @param array $entry The entry being processed. * @param array $form The form the entry belongs to. * * @return bool Returns true if the feed can be processed, otherwise false. */ public function can_process_feed( $feed, $entry, $form ) { $entry_id = (int) rgar( $entry, 'id' ); $processed_feeds = GFAPI::get_processed_feeds_meta( $entry_id, $this->get_slug() ); $already_processed = ! empty( $processed_feeds ) && in_array( (int) rgar( $feed, 'id' ), $processed_feeds ); if ( ! $already_processed ) { return true; } $feed_name = rgars( $feed, 'meta/feed_name' ) ? $feed['meta']['feed_name'] : rgars( $feed, 'meta/feedName' ); if ( ! $this->is_reprocessing_supported( $feed, $entry, $form ) ) { $this->log_debug( __METHOD__ . sprintf( '(): Feed (#%d - %s) has already been processed for entry #%d. Reprocessing is NOT supported.', rgar( $feed, 'id' ), $feed_name, $entry_id ) ); return false; } /** * Allows reprocessing of the feed to be enabled. * * @since 2.9.2 * * @param bool $allow_reprocessing Indicates if the feed can be reprocessed. Default is false. * @param array $feed The feed queued for processing. * @param array $entry The entry being processed. * @param array $form The form the entry belongs to. * @param GFFeedAddOn $addon The current instance of the add-on the feed belongs to. * @param array $processed_feeds An array of feed IDs that have already been processed for the given entry. * * @deprecated 2.9.19 Use gform_allow_feed_reprocessing instead. * */ $allow_reprocessing = apply_filters_deprecated( 'gform_allow_async_feed_reprocessing', array( false, $feed, $entry, $form, $this, $processed_feeds ), '2.9.19', 'gform_allow_feed_reprocessing' ); /** * Allows reprocessing of the feed to be enabled. This applies to both synchronous and asynchronous feed processing. * By default, feeds are prevented from being sent multipe times for the same entry. Using this filter, reprocessing can be enabled. * * @since 2.9.20 * * @param bool $allow_reprocessing Indicates if the feed can be reprocessed. Default is false. * @param array $feed The feed queued for processing. * @param array $entry The entry being processed. * @param array $form The form the entry belongs to. * @param GFFeedAddOn $addon The current instance of the add-on the feed belongs to. * @param array $processed_feeds An array of feed IDs that have already been processed for the given entry. */ $allow_reprocessing = apply_filters( 'gform_allow_feed_reprocessing', false, $feed, $entry, $form, $this, $processed_feeds ); if ( ! $allow_reprocessing ) { $this->log_debug( __METHOD__ . sprintf( '(): Feed (#%d - %s) has already been processed for entry #%d. Reprocessing is NOT allowed.', rgar( $feed, 'id' ), $feed_name, $entry_id ) ); return false; } $this->log_debug( __METHOD__ . sprintf( '(): Feed (#%d - %s) has already been processed for entry #%d. Reprocessing IS allowed.', rgar( $feed, 'id' ), $feed_name, $entry_id ) ); return true; } /** * Determines if the feed should remain in the queue for another attempt based on the error that occurred during processing. * * If overriding in an add-on, call the parent method last to trigger the filter. * * @since 2.9.25 * * @param bool $retry Indicates if the feed can be retried following an error. Default is true. * @param WP_Error $error The error that occurred during feed processing. * @param array $feed The feed that was processed. * @param array $entry The entry the feed was processed for. * @param array $form The form the entry and feed belong to. * * @return bool */ public function is_feed_error_retryable( $retry, $error, $feed, $entry, $form ) { /** * Determines if the feed should remain in the queue for another attempt based on the error that occurred during processing. * * @since 2.9.25 * * @param bool $retry Indicates if the feed can be retried following an error. Default is true. * @param WP_Error $error The error that occurred during feed processing. * @param array $feed The feed that was processed. * @param array $entry The entry the feed was processed for. * @param array $form The form the entry and feed belong to. */ return (bool) apply_filters( 'gform_is_feed_error_retryable', $retry, $error, $feed, $entry, $form ); } /** * Processes feed action. * * @since Unknown * @since 2.9.4 Documented the supported return types for saving the feed status. * * @access public * * @param array $feed The Feed Object currently being processed. * @param array $entry The Entry Object currently being processed. * @param array $form The Form Object currently being processed. * * @return void|null|bool|WP_Error|array The returned value determines if the feed status is saved to the "feed_{$feed_id}_status" entry meta. * - void or null when the feed status should not be saved. * - false or a WP_Error when a failed status should be saved. * - true or the entry array when a success status should be saved. */ public function process_feed( $feed, $entry, $form ) { return; } public function delay_feed( $feed, $entry, $form ) { return; } public function is_feed_condition_met( $feed, $form, $entry ) { $feed_meta = $feed['meta']; $is_condition_enabled = rgar( $feed_meta, 'feed_condition_conditional_logic' ) == true; $logic = rgars( $feed_meta, 'feed_condition_conditional_logic_object/conditionalLogic' ); if ( ! $is_condition_enabled || empty( $logic ) ) { return true; } return GFCommon::evaluate_conditional_logic( $logic, $form, $entry ); } /** * Create nonce for asynchronous feed processing. * * @since 2.2 * @access public * * @return string The nonce. */ public function create_feed_nonce() { $action = 'gform_' . $this->get_slug() . '_process_feed'; $i = wp_nonce_tick(); return substr( wp_hash( $i . $action, 'nonce' ), - 12, 10 ); } /** * Verify nonce for asynchronous feed processing. * * @since 1.0 * @access public * @param string $nonce Nonce to be verified. * * @return int|bool */ public function verify_feed_nonce( $nonce ) { $action = 'gform_' . $this->get_slug() . '_process_feed'; $i = wp_nonce_tick(); // Nonce generated 0-12 hours ago. if ( substr( wp_hash( $i . $action, 'nonce' ), - 12, 10 ) === $nonce ) { return 1; } // Nonce generated 12-24 hours ago. if ( substr( wp_hash( ( $i - 1 ) . $action, 'nonce' ), - 12, 10 ) === $nonce ) { return 2; } // Log that nonce was unable to be verified. $this->log_error( __METHOD__ . '(): Aborting. Unable to verify nonce.' ); return false; } /** * Retrieves notification events supported by Add-On. * * @access public * @param array $form * @return array */ public function supported_notification_events( $form ) { return array(); } /** * Add notifications events supported by Add-On to notification events list. * * @access public * @param array $events * @param array $form * @return array $events */ public function notification_events( $events, $form ) { /* Get the supported notification events for this Add-On. */ $supported_events = $this->supported_notification_events( $form ); /* If no events are supported, return the current array of events. */ if ( empty( $supported_events ) ) { return $events; } return array_merge( $events, $supported_events ); } //-------- Feed data methods ------------------------- /** * Gets the feeds for the specified form id. * * @since Unknown * @since 2.7.17 Added support for decrypting settings fields. * * @param int $form_id The form id to get feeds for. * * @return array Returns an array of feeds for the specified form id. */ public function get_feeds( $form_id = null ) { global $wpdb; $form_filter = is_numeric( $form_id ) ? $wpdb->prepare( 'AND form_id=%d', absint( $form_id ) ) : ''; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gf_addon_feed WHERE addon_slug=%s {$form_filter} ORDER BY `feed_order`, `id` ASC", $this->get_slug() ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching foreach ( $results as &$result ) { $result['meta'] = $this->decrypt_feed_meta( json_decode( $result['meta'], true ) ); } return $results; } /*** * Queries and returns all active feeds for this Add-On * * @since 2.4 * @since 2.7.17 Added support for decrypting settings fields. * * @param int $form_id The Form Id to get feeds from. * * @return array Returns an array with all active feeds associated with the specified form Id */ public function get_active_feeds( $form_id = null ) { global $wpdb; $form_filter = is_numeric( $form_id ) ? $wpdb->prepare( 'AND form_id=%d', absint( $form_id ) ) : ''; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gf_addon_feed WHERE addon_slug=%s AND is_active=1 {$form_filter} ORDER BY `feed_order`, `id` ASC", $this->get_slug() ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching foreach ( $results as &$result ) { $result['meta'] = $this->decrypt_feed_meta( json_decode( $result['meta'], true ) ); } return $results; } /** * Gets the feeds for the specified addon slug and form id. * * @since Unknown * @since 2.7.17 Added support for decrypting settings fields. * * @param string $slug The addon slug to get feeds for. * @param int $form_id (optional) The form id to get feeds for. If not specified, all feeds for the specified addon slug will be returned. * * @return array Returns an array of feeds for the specified form id. */ public function get_feeds_by_slug( $slug, $form_id = null ) { global $wpdb; if ( ! $this->addon_feed_table_exists() ) { $this->show_table_not_exists_error( $wpdb->prefix . 'gf_addon_feed' ); return array(); } $form_filter = is_numeric( $form_id ) ? $wpdb->prepare( 'AND form_id=%d', absint( $form_id ) ) : ''; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gf_addon_feed WHERE addon_slug=%s {$form_filter} ORDER BY `feed_order`, `id` ASC", $slug ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching foreach( $results as &$result ) { $result['meta'] = $this->decrypt_feed_meta( json_decode( $result['meta'], true ) ); } return $results; } public function get_current_feed() { $feed_id = $this->get_current_feed_id(); return empty( $feed_id ) ? false : $this->get_feed( $feed_id ); } public function get_current_feed_id() { if ( $this->_current_feed_id ) { return $this->_current_feed_id; } elseif ( ! rgempty( 'gf_feed_id' ) ) { return rgpost( 'gf_feed_id' ); } else { return rgget( 'fid' ); } } /** * Gets a feed by its id. * * @since Unknown * @since 2.7.17 Added support for decrypting settings fields. * * @param int $id The feed id. * * @return array Returns the feed array if found, false otherwise. */ public function get_feed( $id ) { global $wpdb; if ( ! $this->addon_feed_table_exists() ) { $this->show_table_not_exists_error( $wpdb->prefix . 'gf_addon_feed' ); return false; } $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gf_addon_feed WHERE id=%d", $id ); $row = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( ! $row ) { return false; } $row['meta'] = $this->decrypt_feed_meta( json_decode( $row['meta'], true ) ); return $row; } public function get_feeds_by_entry( $entry_id ) { $processed_feeds = gform_get_meta( $entry_id, 'processed_feeds' ); if ( ! $processed_feeds ) { return false; } return rgar( $processed_feeds, $this->get_slug() ); } public function has_feed( $form_id, $meets_conditional_logic = null ) { $feeds = $this->get_feeds( $form_id ); if ( ! $feeds ) { return false; } $has_active_feed = false; if ( $meets_conditional_logic ) { $form = GFFormsModel::get_form_meta( $form_id ); $entry = GFFormsModel::create_lead( $form ); } foreach ( $feeds as $feed ) { if ( ! $has_active_feed && $feed['is_active'] ) { $has_active_feed = true; } if ( $meets_conditional_logic && $feed['is_active'] && $this->is_feed_condition_met( $feed, $form, $entry ) ) { return true; } } return $meets_conditional_logic ? false : $has_active_feed; } /** * Decrypts the feed meta row and return the decripted array. * * @since 2.7.17 * * @param array $row The feed meta row to decrypt. * * @return array Returns the feed meta row with values decrypted appropriately. */ private function decrypt_feed_meta( $row ) { return $this->get_encryptor()->decrypt_feed_meta( $row ); } public function get_single_submission_feed( $entry = false, $form = false ) { if ( ! $entry && ! $form ) { return false; } $feed = false; if ( ! empty( $this->_single_submission_feed ) && ( ! $form || $this->_single_submission_feed['form_id'] == $form['id'] ) ) { $feed = $this->_single_submission_feed; } elseif ( ! empty( $entry['id'] ) ) { $feeds = $this->get_feeds_by_entry( $entry['id'] ); if ( empty( $feeds ) ) { $feed = $this->get_single_submission_feed_by_form( $form, $entry ); } else { $feed = $this->get_feed( $feeds[0] ); } } elseif ( $form ) { $feed = $this->get_single_submission_feed_by_form( $form, $entry ); $this->_single_submission_feed = $feed; } return $feed; } /** * Return the active feed to be used when processing the current entry, evaluating conditional logic if configured. * * @param array $form The current form. * @param array|false $entry The current entry. * * @return bool|array */ public function get_single_submission_feed_by_form( $form, $entry ) { if ( $form ) { $feeds = $this->get_feeds( $form['id'] ); foreach ( $feeds as $_feed ) { if ( $_feed['is_active'] && $this->is_feed_condition_met( $_feed, $form, $entry ) ) { return $_feed; } } } return false; } /** * Allows the feeds to be filtered before they are processed. * * @since 2.0 * * @param false|array $feeds False or an array of feeds for the current form. * @param array $entry The entry being processed. * @param array $form The form the entry and feeds belong to. * * @return false|array */ public function pre_process_feeds( $feeds, $entry, $form ) { $count = is_array( $feeds ) ? count( $feeds ) : 0; $form_id = (int) rgar( $form, 'id' ); $this->log_debug( __METHOD__ . "(): Found {$count} feeds for form #{$form_id}." ); /** * Modify feeds before they are processed. * * @since 2.0 * * @param false|array $feeds An array of $feed objects * @param array $entry Current entry for which feeds will be processed * @param array $form Current form object. * * @return array An array of $feeds */ $feeds = apply_filters( 'gform_addon_pre_process_feeds', $feeds, $entry, $form ); $feeds = apply_filters( "gform_addon_pre_process_feeds_{$form_id}", $feeds, $entry, $form ); $feeds = apply_filters( "gform_{$this->get_slug()}_pre_process_feeds", $feeds, $entry, $form ); $feeds = apply_filters( "gform_{$this->get_slug()}_pre_process_feeds_{$form_id}", $feeds, $entry, $form ); $filtered_count = is_array( $feeds ) ? count( $feeds ) : 0; if ( $filtered_count !== $count ) { $this->log_debug( __METHOD__ . "(): {$filtered_count} feeds for form #{$form_id} after filters." ); } return $feeds; } /** * Get default feed name. * * @since Unknown * @access public * * @return string */ public function get_default_feed_name() { /** * Query db to look for two formats that the feed name could have been auto-generated with * format from migration to add-on framework: 'Feed ' . $counter * new auto-generated format when adding new feed: $short_title . ' Feed ' . $counter */ // Set to zero unless a new number is found while checking existing feed names (will be incremented by 1 at the end). $counter_to_use = 0; // Get Add-On feeds. $feeds_to_filter = $this->get_feeds_by_slug( $this->get_slug() ); // If feeds were found, loop through and increase counter. if ( $feeds_to_filter ) { // Loop through feeds and look for name pattern to find what to make default feed name. foreach ( $feeds_to_filter as $check ) { // Get feed name and trim. $name = $this->get_feed_name( $check ); $name = trim( $name ); // Prepare feed name pattern. $pattern = '/(^Feed|^' . $this->_short_title . ' Feed)\s\d+/'; // Search for feed name pattern. preg_match( $pattern,$name,$matches ); // If matches were found, increase counter. if ( $matches ) { // Number should be characters at the end after a space $last_space = strrpos( $matches[0], ' ' ); $digit = substr( $matches[0], $last_space ); // Counter in existing feed name greater, use it instead. if ( $digit >= $counter_to_use ){ $counter_to_use = $digit; } } } } // Set default feed name $value = $this->_short_title . ' Feed ' . ($counter_to_use + 1); return $value; } public function is_unique_feed_name( $name, $form_id ) { $feeds = $this->get_feeds( $form_id ); foreach ( $feeds as $feed ) { $feed_name = $this->get_feed_name( $feed ); if ( strtolower( $feed_name ) === strtolower( $name ) ) { return false; } } return true; } /** * Updates the feed meta * * @since Unknown * * @since 2.7.17 Added support for encrypting of settings fields. * * @param int $id Feed ID * @param array $meta Feed meta to be updated * * @return bool */ public function update_feed_meta( $id, $meta ) { global $wpdb; $meta = $this->get_encryptor()->encrypt_feed_meta( $meta, $this->get_fields_to_encrypt() ); $meta = json_encode( $meta ); $wpdb->update( "{$wpdb->prefix}gf_addon_feed", array( 'meta' => $meta ), array( 'id' => $id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->rows_affected > 0; } public function update_feed_active( $id, $is_active ) { global $wpdb; $is_active = $is_active ? '1' : '0'; $wpdb->update( "{$wpdb->prefix}gf_addon_feed", array( 'is_active' => $is_active ), array( 'id' => $id ), array( '%d' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $success = $wpdb->rows_affected > 0; /* * Do an action after a feed status has been updated. * * @since 2.9.20 * * @param int $id The ID of the feed being updated. * @param bool $is_active The new active status of the feed. * @param GFFeedAddOn $this The current instance of the add-on for which the feed is being updated. */ do_action( 'gform_update_feed_active', $id, $is_active, $this ); return $success; } /** * Insert a new feed record. * * @since Unknown * * @since 2.7.17 Added support for encrypting settings fields. * * @param int $form_id Form ID. * @param bool $is_active If the feed is active or not. * @param array $meta Feed meta * * @return false|int Returns the ID of the newly created feed or false if the feed table does not exist. */ public function insert_feed( $form_id, $is_active, $meta ) { global $wpdb; if ( ! $this->addon_feed_table_exists() ) { $this->show_table_not_exists_error( $wpdb->prefix . 'gf_addon_feed' ); return false; } $meta = $this->get_encryptor()->encrypt_feed_meta( $meta, $this->get_fields_to_encrypt() ); $meta = json_encode( $meta ); $wpdb->insert( "{$wpdb->prefix}gf_addon_feed", array( 'addon_slug' => $this->get_slug(), 'form_id' => $form_id, 'is_active' => $is_active, 'meta' => $meta ), array( '%s', '%d', '%d', '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->insert_id; } /** * Get the array of feed settings field names that are configured to be encrypted. * * @since 2.7.16 * * @return array Returns an array with all field names that are configured to be encrypted. */ public function get_fields_to_encrypt() { static $cached_fields_to_encrypt; if ( rgar( $cached_fields_to_encrypt, $this->_slug ) ) { return $cached_fields_to_encrypt[ $this->_slug ]; } $groups = $this->get_feed_settings_fields(); // Loop through feed settings fields and create array of fields that are configured to be encrypted $fields_to_encrypt = array(); foreach ( $groups as $group ) { if ( ! isset( $group['fields'] ) ) { continue; } foreach ( $group['fields'] as $field ) { if ( rgar( $field, 'encrypt' ) ) { $fields_to_encrypt[] = $field['name']; } } } $cached_fields_to_encrypt[ $this->_slug ] = $fields_to_encrypt; return $fields_to_encrypt; } public function delete_feed( $id ) { global $wpdb; /** * Allows custom actions to be performed just before a feed is deleted from the database. * * @since 2.4.21 * * @param int $id The ID of the feed being deleted. * @param GFFeedAddOn $this The current instance of the add-on for which the feed is being deleted. */ do_action( 'gform_pre_delete_feed', $id, $this ); do_action( "gform_{$this->get_short_slug()}_pre_delete_feed", $id, $this ); $wpdb->delete( "{$wpdb->prefix}gf_addon_feed", array( 'id' => $id ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } public function delete_feeds( $form_id = null ) { global $wpdb; $form_filter = is_numeric( $form_id ) ? $wpdb->prepare( 'AND form_id=%d', absint( $form_id ) ) : ''; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $sql = $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_addon_feed WHERE addon_slug=%s {$form_filter} ORDER BY `feed_order`, `id` ASC", $this->get_slug() ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $feed_ids = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( ! empty( $feed_ids ) ) { array_map( array( $this, 'delete_feed' ), $feed_ids ); } } /** * Duplicates the feed. * * @since 1.9.15 * @access public * * @param int|array $id The ID of the feed to be duplicated or the feed object when duplicating a form. * @param mixed $new_form_id False when using feed actions or the ID of the new form when duplicating a form. * * @uses GFFeedAddOn::can_duplicate_feed() * @uses GFFeedAddOn::get_feed() * @uses GFFeedAddOn::insert_feed() * @uses GFFeedAddOn::is_unique_feed_name() * * @return int New feed ID. */ public function duplicate_feed( $id, $new_form_id = false ) { // Get original feed. $original_feed = is_array( $id ) ? $id : $this->get_feed( $id ); // If feed doesn't exist, exit. if ( ! $original_feed || ! $this->can_duplicate_feed( $original_feed ) ) { return; } // Get feed name key. $feed_name_key = rgars( $original_feed, 'meta/feed_name' ) ? 'feed_name' : 'feedName'; $original_feed_name = $this->get_feed_name( $original_feed, $feed_name_key ); // Make sure the new feed name is unique. $count = 2; $feed_name = $original_feed_name . ' - ' . esc_html__( 'Copy 1', 'gravityforms' ); while ( ! $this->is_unique_feed_name( $feed_name, $original_feed['form_id'] ) ) { $feed_name = $original_feed_name . ' - ' . sprintf( esc_html__( 'Copy %d', 'gravityforms' ), $count ); $count++; } // Copy the feed meta. $meta = $original_feed['meta']; $meta[ $feed_name_key ] = $feed_name; if ( ! $new_form_id ) { $new_form_id = $original_feed['form_id']; } // Create the new feed. return $this->insert_feed( $new_form_id, $original_feed['is_active'], $meta ); } /** * Checks if Addon Feed table exists. * * @since 2.4.23 * * @return bool If Addon Feed table exists. */ private function addon_feed_table_exists() { global $wpdb; return $this->table_exists( $wpdb->prefix . 'gf_addon_feed' ); } /** * Get the Table does not exist error message. * * @since 2.4.23 * * @param string $table The missing table name. */ private function get_table_not_exists_error( $table ) { $status_page_url = admin_url( 'admin.php?page=gf_system_status' ); return sprintf( // translators: %1$s represents the missing table, %2$s is the opening link tag, %3$s is the closing link tag. esc_html__( 'The table `%1$s` does not exist. Please visit the %2$sForms > System Status%3$s page and click the "Re-run database upgrade" link (under the Database section) to create the missing table.', 'gravityforms' ), esc_html( $table ), '', '' . esc_html__('(opens in a new tab)', 'gravityforms') . ' ' ); } /** * Output a Table does not exist error message. * * @since 2.4.23 * * @param string $table The missing table name. */ private function show_table_not_exists_error( $table ) { // Prevent the error from being displayed more than once. if ( ! empty( $this->_table_error_rendered[ $table ] ) ) { return; } $error = $this->get_table_not_exists_error( $table ); $classes = $this->is_gravityforms_supported( '2.5-beta' ) ? 'notice notice-error gf-notice' : 'notice notice-error'; $notice = sprintf( '
%s
%s
', $title, esc_html__( 'Unable to render feed settings.', 'gravityforms' ) ); // phpcs:ignore WordPress.Security.EscapeOutput return; } $this->get_settings_renderer()->render(); } public function settings( $sections ) { parent::settings( $sections ); ?> addon_feed_table_exists() ) { $this->show_table_not_exists_error( $wpdb->prefix . 'gf_addon_feed' ); return; } $action = $this->get_bulk_action(); if ( $action ) { check_admin_referer( 'feed_list', 'feed_list' ); $this->process_bulk_action( $action ); } $single_action = rgpost( 'single_action' ); if ( ! empty( $single_action ) ) { check_admin_referer( 'feed_list', 'feed_list' ); $this->process_single_action( $single_action ); } ?>