8889841cPKE ['.?11Queue_Processor.phpnu[manage_scheduled_task(); } /** * Configures a scheduled task to handle "background processing" of import record insertions/updates. */ public function manage_scheduled_task() { add_action( 'tribe_events_blog_deactivate', [ $this, 'clear_scheduled_task' ] ); add_action( self::$scheduled_key, [ $this, 'process_queue' ], 20, 0 ); add_action( self::$scheduled_single_key, [ $this, 'process_queue' ], 20, 0 ); $this->register_scheduled_task(); } /** * Runs upon plugin activation, registering our scheduled task used to process * batches of pending import record inserts/updates. */ public function register_scheduled_task() { // Bail on registration of scheduled event in case we don't have an API setup. if ( is_wp_error( tribe( 'events-aggregator.service' )->api() ) ) { // Also clear in case we don't have an API key. $this->clear_scheduled_task(); return; } // Prevent from trying to schedule in case we don't have any scheduled records to process, value will either be false or 0. if ( ! $this->next_waiting_record( false, true ) ) { // Also clear in case we don't have any records to process. $this->clear_scheduled_task(); return; } // If we have one scheduled, don't schedule another. if ( wp_next_scheduled( self::$scheduled_key ) ) { return; } /** * Filter the interval at which to process import records. * * By default a custom interval of every 15mins is specified, however * other intervals such as "hourly", "twicedaily" and "daily" can * normally be substituted. * * @see wp_schedule_event() * @see 'cron_schedules' */ $interval = apply_filters( 'tribe_aggregator_record_processor_interval', 'tribe-every15mins' ); wp_schedule_event( time(), $interval, self::$scheduled_key ); } /** * Fires upon plugin deactivation. */ public function clear_scheduled_task() { wp_clear_scheduled_hook( self::$scheduled_key ); } /** * Process a batch of queued items for a specific import record. * * This is typically used when processing a small number of instances immediately upon * an import record queue being updated for a particular import record, or to facilitate * batches being updated via an ajax update loop. * * The default number of items processed in a single batch is 10, which can be * overridden using the tribe_events_aggregator_small_batch_size filter hook * * @param int $record_id * @param int $batch_size */ public function process_batch( $record_id, $batch_size = null ) { /** * Sets the default number of instances to be immediately processed when a record has items to insert * * @param int $small_batch_size */ $default_batch_size = apply_filters( 'tribe_aggregator_small_batch_size', self::$small_batch_size ); self::$batch_size = ( null === $batch_size ) ? $default_batch_size : (int) $batch_size; $this->current_record_id = (int) $record_id; $this->do_processing(); } /** * Processes the next waiting batch of Import Record posts, if there are any. * * @param int $batch_size */ public function process_queue( $batch_size = null ) { if ( null === $batch_size ) { /** * Controls the size of each batch processed by default (ie, during cron updates of record * inserts/updates). * * @param int $default_batch_size */ self::$batch_size = (int) apply_filters( 'tribe_aggregator_batch_size', self::$batch_size ); } else { self::$batch_size = (int) $batch_size; } while ( $this->next_waiting_record() ) { if ( ! $this->do_processing() ) { break; } } $queue_items = get_post_meta( $this->current_record_id, Tribe__Events__Aggregator__Records::instance()->prefix_meta( Tribe__Events__Aggregator__Record__Queue::$queue_key ), true ); // We only get here if we done processing this batch // Now we will check for more events on the queue if ( ! empty( $queue_items ) ) { // Schedule a Cron Event to happen ASAP, and flag it for searching and we need to make it unique // By default WordPress won't allow more than one Action to happen twice in 10 minutes wp_schedule_single_event( time(), self::$scheduled_single_key ); } } public function set_current_queue( Tribe__Events__Aggregator__Record__Queue_Interface $queue ) { $this->current_queue = $queue; } /** * Obtains the post ID of the next record which has a queue of items in need * of processing. * * If no records in need of further processing can be found it will return bool false. * * @since 5.3.0 Inclusion of a $cache param for performance purposes. * * @param boolean $interactive_only Whether or not we should look for imports that were kicked off interactively * @param boolean $cache When checking on every request we should utilize transient caching to prevent hitting the DB every time. * * @return boolean|integer */ public function next_waiting_record( $interactive_only = false, $cache = false ) { if ( true === $cache ) { $interactive_only_suffix = ''; if ( $interactive_only ) { $interactive_only_suffix = '_interactive_only'; } $transient_key = 'tribe-event-aggregator-next_waiting_record' . ( ! $interactive_only ? '' : '_interactive_only' ); $next_waiting_record = get_transient( $transient_key ); if ( ! empty( $next_waiting_record ) ) { return $this->current_record_id = $next_waiting_record; } elseif ( null === $next_waiting_record ) { // When not false we return false; } } $args = [ 'post_type' => Tribe__Events__Aggregator__Records::$post_type, 'post_status' => 'any', 'posts_per_page' => 1, 'meta_query' => [ [ 'key' => Tribe__Events__Aggregator__Record__Abstract::$meta_key_prefix . Tribe__Events__Aggregator__Record__Queue::$queue_key, 'compare' => 'EXISTS', ], ], ]; if ( $interactive_only ) { $args['meta_query'][] = [ 'key' => Tribe__Events__Aggregator__Record__Abstract::$meta_key_prefix . 'interactive', 'compare' => 'EXISTS', ]; } $waiting_records = get_posts( $args ); if ( empty( $waiting_records ) ) { // Set cache in case of usage. if ( true === $cache ) { // Setting to null prevents us from running for 5 minutes. set_transient( $transient_key, null, 5 * MINUTE_IN_SECONDS ); } return $this->current_record_id = 0; } $next_record = array_shift( $waiting_records ); // Set cache in case of usage. if ( true === $cache ) { set_transient( $transient_key, $next_record->ID, 5 * MINUTE_IN_SECONDS ); } return $this->current_record_id = $next_record->ID; } /** * Processes the current import record queue. May return boolean false if it is unable to continue. * * @return bool */ protected function do_processing() { // Bail out if the batch limit has been exceeded, if nothing is waiting in the queue // or the queue is actively being processed by a concurrent request/scheduled task if ( $this->batch_complete() || ! $this->get_current_queue() || $this->current_queue->is_in_progress() ) { return false; } $this->current_queue->set_in_progress_flag(); $processed = $this->current_queue->process( self::$batch_size ); // in the 'fetch' phase this will not be a Queue object if ( $processed instanceof Tribe__Events__Aggregator__Record__Queue_Interface ) { $this->processed += $processed->activity->count( $this->current_queue->get_queue_type() ); } $this->current_queue->clear_in_progress_flag(); return true; } /** * Returns true if a non-empty queue exists for the current record, else returns false. * * @return bool */ protected function get_current_queue() { try { $this->current_queue = self::build_queue( $this->current_record_id ); } catch ( InvalidArgumentException $e ) { do_action( 'log', sprintf( __( 'Could not process queue for Import Record %1$d: %2$s', 'the-events-calendar' ), $this->current_record_id, $e->getMessage() ) ); return false; } if ( $this->current_queue->is_stuck() || $this->current_queue->has_errors() ) { $this->current_queue->kill_queue(); return false; } return $this->current_queue->is_empty() ? false : true; } /** * Determines if the batch job is complete. * * Currently this is simply a measure of the number of instances processed against * the batch size limit - however it could potentially be expanded to include an * additional time based check. * * @return bool */ protected function batch_complete() { return ( $this->processed >= self::$batch_size ); } /** * Builds the correct class of queue. * * @since 4.6.16 * * @param int|Tribe__Events__Aggregator__Record__Abstract $record A record object or ID * @param array|string $items * @param bool $use_legacy Whether to use the legacy queue processor or not. * * @return Tribe__Events__Aggregator__Record__Queue_Interface */ public static function build_queue( $record, $items = null, $use_legacy = false ) { if ( ( defined( 'TRIBE_EA_QUEUE_USE_LEGACY' ) && TRIBE_EA_QUEUE_USE_LEGACY ) || (bool) getenv( 'TRIBE_EA_QUEUE_USE_LEGACY' ) || false !== (bool) tribe_get_request_var( 'tribe_ea_queue_use_legacy', false ) ) { $use_legacy = true; } if ( is_numeric( $record ) ) { $record = tribe( 'events-aggregator.records' )->get_by_post_id( $record ); } if ( ! $record instanceof Tribe__Events__Aggregator__Record__Abstract ) { if ( $record instanceof WP_Error ) { return new Tribe__Events__Aggregator__Record__Void_Queue( $record ); } return new Tribe__Events__Aggregator__Record__Void_Queue( __( 'There was an error building the record queue: ' . print_r( $record, true ) ) ); } /** @var Tribe__Events__Aggregator__Settings $settings */ $settings = tribe( 'events-aggregator.settings' ); $class = $settings->get_import_process_class(); // Force the use of the Legacy Queue for CSV Imports if ( $record instanceof Tribe__Events__Aggregator__Record__CSV || $use_legacy ) { $class = Tribe__Events__Aggregator__Record__Queue::class; } // If the current Queue is a cron Queue or a Batch Queue. $is_batch_queue = ( Tribe__Events__Aggregator__Record__Queue::class === $class || Batch_Queue::class === $class ); $use_batch_queue = ( $use_legacy || $is_batch_queue ); if ( $use_batch_queue && ! empty( $record->meta ) && ! empty( $record->meta['allow_batch_push'] ) && tribe_is_truthy( $record->meta['allow_batch_push'] ) ) { $class = Batch_Queue::class; } /** * Filters the class of the queue that should be used. * * This filter can also return a fully built queue object. * * @since 4.6.16 * * @param string $class The import process class that will be used to process * import records. * @param Tribe__Events__Aggregator__Record__Abstract $record The current record being processed. * @param array|string $items Either an array of the record items to process or a string * to indicate pre-process states like fetch or on-hold. */ $class = apply_filters( 'tribe_aggregator_queue_class', $class, $record, $items ); return $class instanceof Tribe__Events__Aggregator__Record__Queue_Interface ? $class : new $class( $record, $items ); } } PKE [$UU Abstract.phpnu[ [ 'show_map_link' ], ]; public static $unique_id_fields = [ 'meetup' => [ 'source' => 'meetup_id', 'target' => 'EventMeetupID', ], 'eventbrite' => [ 'source' => 'eventbrite_id', 'target' => 'EventBriteID', ], 'ical' => [ 'source' => 'uid', 'target' => 'uid', ], 'gcal' => [ 'source' => 'uid', 'target' => 'uid', ], 'ics' => [ 'source' => 'uid', 'target' => 'uid', ], 'url' => [ 'source' => 'id', 'target' => 'EventOriginalID', ], ]; /** * @var array */ public static $unique_venue_id_fields = [ 'meetup' => [ 'source' => 'meetup_id', 'target' => 'VenueMeetupID', ], 'eventbrite' => [ 'source' => 'eventbrite_id', 'target' => 'VenueEventBriteID', ], ]; /** * @var array */ public static $unique_organizer_id_fields = [ 'meetup' => [ 'source' => 'meetup_id', 'target' => 'OrganizerMeetupID', ], 'eventbrite' => [ 'source' => 'eventbrite_id', 'target' => 'OrganizerEventBriteID', ], ]; /** * Cache variable to store the last child post. * * @var WP_Post */ protected $last_child; /** * Holds the event count temporarily while event counts (comment_count) is being updated * * @var int */ private $temp_event_count = 0; /** * The import record origin. * * @var string */ public $origin; /** * Setup all the hooks and filters * * @return void */ public function __construct( $post = null ) { $this->image_uploader = new Tribe__Image__Uploader(); // If we have an Post we try to Setup $this->load( $post ); } /** * Public facing Label for this Origin * * @return string */ abstract public function get_label(); /** * Loads the WP_Post associated with this record */ public function load( $post = null ) { if ( is_numeric( $post ) ) { $post = get_post( $post ); } if ( ! $post instanceof WP_Post ) { return tribe_error( 'core:aggregator:invalid-record-object', [], [ $post ] ); } if ( Records::$post_type !== $post->post_type ) { return tribe_error( 'core:aggregator:invalid-record-post_type', [], [ $post ] ); } $this->id = $post->ID; // Get WP_Post object $this->post = $post; // Map `ping_status` as the `type` $this->type = $this->post->ping_status; if ( 'schedule' === $this->type ) { // Fetches the Frequency Object $this->frequency = Tribe__Events__Aggregator__Cron::instance()->get_frequency( [ 'id' => $this->post->post_content ] ); // Boolean Flag for Scheduled records $this->is_schedule = true; } else { // Everything that is not a Scheduled Record is set as Manual $this->is_manual = true; } $this->setup_meta( get_post_meta( $this->id ) ); return $this; } /** * Sets up meta fields by de-prefixing them into the array * * @param array $meta Meta array */ public function setup_meta( $meta ) { foreach ( $meta as $key => $value ) { $key = preg_replace( '/^' . self::$meta_key_prefix . '/', '', $key ); $this->meta[ $key ] = maybe_unserialize( is_array( $value ) ? reset( $value ) : $value ); } // `source` will be empty when importing .ics files $this->meta['source'] = ! empty ( $this->meta['source'] ) ? $this->meta['source'] : ''; $original_source = $this->meta['source']; // Intelligently prepend "http://" if the protocol is missing from the source URL if ( ! empty( $this->meta['source'] ) && false === strpos( $this->meta['source'], '://' ) ) { $this->meta['source'] = 'http://' . $this->meta['source']; } /** * Provides an opportunity to set or modify the source URL for an import. * * @since 4.5.11 * * @param string $source * @param string $original_source * @param WP_Post $record * @param array $meta */ $this->meta['source'] = apply_filters( 'tribe_aggregator_meta_source', $this->meta['source'], $original_source, $this->post, $this->meta ); // This prevents lots of isset checks for no reason if ( empty( $this->meta['activity'] ) ) { $this->meta['activity'] = new Tribe__Events__Aggregator__Record__Activity(); } } /** * Updates import record meta * * @param string $key Meta key * @param mixed $value Meta value */ public function update_meta( $key, $value ) { $this->meta[ $key ] = $value; $field = self::$meta_key_prefix . $key; if ( null === $value ) { return delete_post_meta( $this->post->ID, $field ); } return update_post_meta( $this->post->ID, $field, $value ); } /** * Deletes import record meta * * @param string $key Meta key */ public function delete_meta( $key ) { unset( $this->meta[ $key ] ); return delete_post_meta( $this->post->ID, self::$meta_key_prefix . $key ); } /** * Returns the Activity object for the record * * @return Tribe__Events__Aggregator__Record__Activity */ public function activity() { if ( empty( $this->meta['activity'] ) ) { $activity = new Tribe__Events__Aggregator__Record__Activity; $this->update_meta( 'activity', $activity ); } return $this->meta['activity']; } /** * Saves activity data on a record */ public function save_activity() { $this->update_meta( 'activity', $this->activity() ); } /** * Gets a hash with the information we need to verify if a given record is a duplicate * * @since 4.5.13 * * @return string */ public function get_data_hash() { $meta = [ 'file', 'keywords', 'location', 'start', 'end', 'radius', 'source', 'content_type', ]; $data = [ 'type' => $this->type, 'origin' => $this->origin, 'frequency' => null, ]; // If schedule Record, we need it's frequency if ( $this->is_schedule ) { $data['frequency'] = $this->frequency->id; } foreach ( $meta as $meta_key ) { if ( ! isset( $this->meta[ $meta_key ] ) ) { continue; } $data[ $meta_key ] = $this->meta[ $meta_key ]; } // Remove the empty Keys $data = array_filter( $data ); // Sort to avoid any weird MD5 stuff ksort( $data ); // Create a string to be able to MD5 $data_string = maybe_serialize( $data ); return md5( $data_string ); } /** * Creates an import record * * @param string $type Type of record to create - manual or schedule * @param array $args Post type args * @param array $meta Post meta * * @return WP_Post|WP_Error */ public function create( $type = 'manual', $args = [], $meta = [] ) { if ( ! in_array( $type, [ 'manual', 'schedule' ] ) ) { return tribe_error( 'core:aggregator:invalid-create-record-type', $type ); } $defaults = [ 'parent' => 0, ]; $args = (object) wp_parse_args( $args, $defaults ); $defaults = [ 'frequency' => null, 'hash' => wp_generate_password( 32, true, true ), 'preview' => false, 'allow_multiple_organizers' => true, ]; $meta = wp_parse_args( $meta, $defaults ); $post = $this->prep_post_args( $type, $args, $meta ); $this->watch_for_db_errors(); $result = wp_insert_post( $post ); if ( is_wp_error( $result ) ) { $this->maybe_add_meta_via_pre_wp_44_method( $result, $post['meta_input'] ); } if ( $this->db_errors_happened() ) { $error_message = __( 'Something went wrong while inserting the record in the database.', 'the-events-calendar' ); wp_delete_post( $result ); return new WP_Error( 'db-error-during-creation', $error_message ); } // After Creating the Post Load and return return $this->load( $result ); } /** * Edits an import record * * @param int $post_id * @param array $args Post type args * @param array $meta Post meta * * @return WP_Post|WP_Error */ public function save( $post_id, $args = [], $meta = [] ) { if ( ! isset( $meta['type'] ) || 'schedule' !== $meta['type'] ) { return tribe_error( 'core:aggregator:invalid-edit-record-type', $meta ); } $defaults = [ 'parent' => 0, ]; $args = (object) wp_parse_args( $args, $defaults ); $defaults = [ 'frequency' => null, ]; $meta = wp_parse_args( $meta, $defaults ); $post = $this->prep_post_args( $meta['type'], $args, $meta ); $post['ID'] = absint( $post_id ); $post['post_status'] = Records::$status->schedule; add_filter( 'wp_insert_post_data', [ $this, 'dont_change_post_modified' ], 10, 2 ); $result = wp_update_post( $post ); remove_filter( 'wp_insert_post_data', [ $this, 'dont_change_post_modified' ] ); if ( ! is_wp_error( $result ) ) { $this->maybe_add_meta_via_pre_wp_44_method( $result, $post['meta_input'] ); } // After Creating the Post Load and return return $this->load( $result ); } /** * Filter the post_modified dates to be unchanged * conditionally hooked to wp_insert_post_data and then unhooked after wp_update_post * * @param array $data new data to be used in the update * @param array $postarr existing post data * * @return array */ public function dont_change_post_modified( $data, $postarr ) { $post = get_post( $postarr['ID'] ); $data['post_modified'] = $postarr['post_modified']; $data['post_modified_gmt'] = $postarr['post_modified_gmt']; return $data; } /** * Preps post arguments for create/save * * @param string $type Type of record to create - manual or schedule. * @param object $args Post type args. * @param array $meta Post meta. * * @return array */ public function prep_post_args( $type, $args, $meta = [] ) { $post = [ 'post_title' => $this->generate_title( $type, $this->origin, $meta['frequency'], $args->parent ), 'post_type' => Records::$post_type, 'ping_status' => $type, // The Mime Type needs to be on a %/% format to work on WordPress 'post_mime_type' => 'ea/' . $this->origin, 'post_date' => current_time( 'mysql' ), 'post_status' => Records::$status->draft, 'post_parent' => $args->parent, 'meta_input' => [], ]; // prefix all keys foreach ( $meta as $key => $value ) { // skip arrays that are empty if ( is_array( $value ) && empty( $value ) ) { continue; } // trim scalars if ( is_scalar( $value ) ) { $value = trim( $value ); } // if the value is null, let's avoid inserting it if ( null === $value ) { continue; } $post['meta_input'][ self::$meta_key_prefix . $key ] = $value; } $meta = (object) $meta; if ( 'schedule' === $type ) { $frequency = Tribe__Events__Aggregator__Cron::instance()->get_frequency( [ 'id' => $meta->frequency ] ); if ( ! $frequency ) { return tribe_error( 'core:aggregator:invalid-record-frequency', $meta ); } // Setup the post_content as the Frequency (makes it easy to fetch by frequency) $post['post_content'] = $frequency->id; } return $post; } /** * A simple method to create a Title for the Records * * @param mixed $Nparams This method accepts any number of params, they must be string compatible * * @return string */ public function generate_title() { $parts = func_get_args(); return __( 'Record: ', 'the-events-calendar' ) . implode( ' ', array_filter( $parts ) ); } /** * Creates a schedule record based on the import record * * @return boolean|Tribe_Error */ public function create_schedule_record() { $post = [ 'post_title' => $this->generate_title( $this->type, $this->origin, $this->meta['frequency'] ), 'post_type' => $this->post->post_type, 'ping_status' => $this->post->ping_status, 'post_mime_type' => $this->post->post_mime_type, 'post_date' => current_time( 'mysql' ), 'post_status' => Records::$status->schedule, 'post_parent' => 0, 'meta_input' => [], ]; foreach ( $this->meta as $key => $value ) { // don't propagate these meta keys to the scheduled record if ( 'preview' === $key || 'activity' === $key || 'ids_to_import' === $key ) { continue; } $post['meta_input'][ self::$meta_key_prefix . $key ] = $value; } // associate this child with the schedule $post['meta_input'][ self::$meta_key_prefix . 'recent_child' ] = $this->post->ID; $frequency = Tribe__Events__Aggregator__Cron::instance()->get_frequency( [ 'id' => $this->meta['frequency'] ] ); if ( ! $frequency ) { return tribe_error( 'core:aggregator:invalid-record-frequency', $this->meta ); } // Setups the post_content as the Frequency (makes it easy to fetch by frequency) $post['post_content'] = $frequency->id; $this->watch_for_db_errors(); // create schedule post $schedule_id = wp_insert_post( $post ); // if the schedule creation failed, bail if ( is_wp_error( $schedule_id ) ) { return tribe_error( 'core:aggregator:save-schedule-failed' ); } $this->maybe_add_meta_via_pre_wp_44_method( $schedule_id, $post['meta_input'] ); if ( $this->db_errors_happened() ) { wp_delete_post( $schedule_id ); return tribe_error( 'core:aggregator:save-schedule-failed' ); } $update_args = [ 'ID' => $this->post->ID, 'post_parent' => $schedule_id, ]; // update the parent of the import we are creating the schedule for. If that fails, delete the // corresponding schedule and bail if ( ! wp_update_post( $update_args ) ) { wp_delete_post( $schedule_id, true ); return tribe_error( 'core:aggregator:save-schedule-failed' ); } $this->post->post_parent = $schedule_id; return Records::instance()->get_by_post_id( $schedule_id ); } /** * Creates a child record based on the import record * * @return boolean|Tribe_Error|Tribe__Events__Aggregator__Record__Abstract */ public function create_child_record() { $frequency_id = 'on_demand'; if ( ! empty( $this->meta['frequency'] ) ) { $frequency_id = $this->meta['frequency']; } $post = [ // Stores the Key under `post_title` which is a very forgiving type of column on `wp_post` 'post_title' => $this->generate_title( $this->type, $this->origin, $frequency_id, $this->post->ID ), 'post_type' => $this->post->post_type, 'ping_status' => $this->post->ping_status, 'post_mime_type' => $this->post->post_mime_type, 'post_date' => current_time( 'mysql' ), 'post_status' => Records::$status->draft, 'post_parent' => $this->id, 'post_author' => $this->post->post_author, 'meta_input' => [], ]; foreach ( $this->meta as $key => $value ) { if ( 'activity' === $key ) { // don't copy the parent activity into the child record continue; } $post['meta_input'][ self::$meta_key_prefix . $key ] = $value; } // initialize the queue meta entry and set its status to fetching $post['meta_input'][ self::$meta_key_prefix . Tribe__Events__Aggregator__Record__Queue::$queue_key ] = 'fetch'; $frequency = Tribe__Events__Aggregator__Cron::instance()->get_frequency( [ 'id' => $frequency_id ] ); if ( ! $frequency ) { return tribe_error( 'core:aggregator:invalid-record-frequency', $post['meta_input'] ); } // Setup the post_content as the Frequency (makes it easy to fetch by frequency) $post['post_content'] = $frequency->id; $this->watch_for_db_errors(); // create schedule post $child_id = wp_insert_post( $post ); // if the schedule creation failed, bail if ( is_wp_error( $child_id ) ) { return tribe_error( 'core:aggregator:save-child-failed' ); } $this->maybe_add_meta_via_pre_wp_44_method( $child_id, $post['meta_input'] ); if ( $this->db_errors_happened() ) { wp_delete_post( $child_id ); return tribe_error( 'core:aggregator:save-child-failed' ); } // track the most recent child that was spawned $this->update_meta( 'recent_child', $child_id ); return Records::instance()->get_by_post_id( $child_id ); } /** * If using WP < 4.4, we need to add meta to the post via update_post_meta * * @param int $id Post id to add data to * @param array $meta Meta to add to the post */ public function maybe_add_meta_via_pre_wp_44_method( $id, $meta ) { if ( -1 !== version_compare( get_bloginfo( 'version' ), '4.4' ) ) { return; } foreach ( $meta as $key => $value ) { update_post_meta( $id, $key, $value ); } } /** * Queues the import on the Aggregator service * * @see Tribe__Events__Aggregator__API__Import::create() * * @return stdClass|WP_Error|int A response object, a `WP_Error` instance on failure or a record * post ID if the record had to be re-scheduled due to HTTP request * limit. */ public function queue_import( $args = [] ) { $aggregator = tribe( 'events-aggregator.main' ); $is_previewing = ( ! empty( $_GET['action'] ) && ( 'tribe_aggregator_create_import' === $_GET['action'] || 'tribe_aggregator_preview_import' === $_GET['action'] ) ); $error = null; $defaults = [ 'type' => $this->meta['type'], 'origin' => $this->meta['origin'], 'source' => isset( $this->meta['source'] ) ? $this->meta['source'] : '', 'callback' => $is_previewing ? null : home_url( '/event-aggregator/insert/?key=' . urlencode( $this->meta['hash'] ) ), 'resolve_geolocation' => 1, ]; if ( ! empty( $this->meta['frequency'] ) ) { $defaults['frequency'] = $this->meta['frequency']; } if ( ! empty( $this->meta['file'] ) ) { $defaults['file'] = $this->meta['file']; } if ( ! empty( $this->meta['keywords'] ) ) { $defaults['keywords'] = $this->meta['keywords']; } if ( ! empty( $this->meta['location'] ) ) { $defaults['location'] = $this->meta['location']; } if ( ! empty( $this->meta['start'] ) ) { $defaults['start'] = $this->meta['start']; } if ( ! empty( $this->meta['end'] ) ) { $defaults['end'] = $this->meta['end']; } if ( ! empty( $this->meta['radius'] ) ) { $defaults['radius'] = $this->meta['radius']; } if ( ! empty( $this->meta['allow_multiple_organizers'] ) ) { $defaults['allow_multiple_organizers'] = $this->meta['allow_multiple_organizers']; } if ( empty( $this->meta['next_batch_hash'] ) ) { $next_batch_hash = $this->generate_next_batch_hash(); $defaults['next_batch_hash'] = $next_batch_hash; $this->update_meta( 'next_batch_hash', $next_batch_hash ); } if ( $is_previewing ) { $defaults['preview'] = true; } $args = wp_parse_args( $args, $defaults ); if ( ! empty( $args['start'] ) ) { $args['start'] = ! is_numeric( $args['start'] ) ? Dates::maybe_format_from_datepicker( $args['start'] ) : Dates::build_date_object( $args['start'] )->format( Dates::DBDATETIMEFORMAT ); } if ( ! empty( $args['end'] ) ) { $args['end'] = ! is_numeric( $args['end'] ) ? Dates::maybe_format_from_datepicker( $args['end'] ) : Dates::build_date_object( $args['end'] )->format( Dates::DBDATETIMEFORMAT ); } // Set site for origin(s) that need it for new token handling. if ( in_array( $args['origin'], [ 'eventbrite', 'facebook-dev' ], true ) ) { $args['site'] = site_url(); } /** * Allows customizing whether to resolve geolocation for events by the EA service. * * @since 4.6.25 * * @param boolean $resolve_geolocation Whether the EA Geocode Address API is enabled for geocoding addresses. * @param array $args Queued record import arguments to be sent to EA service. */ $resolve_geolocation = apply_filters( 'tribe_aggregator_resolve_geolocation', true, $args ); if ( false === $resolve_geolocation ) { $args['resolve_geolocation'] = 0; } // create the import on the Event Aggregator service $response = $aggregator->api( 'import' )->create( $args ); $foo = ''; // if the Aggregator API returns a WP_Error, set this record as failed if ( is_wp_error( $response ) ) { // if the error is just a reschedule set this record as pending /** @var WP_Error $response */ if ( 'core:aggregator:http_request-limit' === $response->get_error_code() ) { $this->should_queue_import( true ); return $this->set_status_as_pending(); } $error = $response; tribe( 'logger' )->log_debug( 'Error during the queue of the record.', 'EA Queue Import' ); return $this->set_status_as_failed( $error ); } // if the Aggregator response has an unexpected format, set this record as failed if ( empty( $response->message_code ) ) { tribe( 'logger' )->log_debug( 'Response code is empty.', 'EA Abstract' ); return $this->set_status_as_failed( tribe_error( 'core:aggregator:invalid-service-response' ) ); } // if the Import creation was unsuccessful, set this record as failed if ( 'success:create-import' != $response->message_code && 'queued' != $response->message_code ) { $data = ! empty( $response->data ) ? $response->data : []; $error = new WP_Error( $response->message_code, Tribe__Events__Aggregator__Errors::build( esc_html__( $response->message, 'the-events-calendar' ), $data ), $data ); tribe( 'logger' )->log_debug( 'Error when the creation of the import is taking place.', 'EA Queue Import' ); return $this->set_status_as_failed( $error ); } // if the Import creation didn't provide an import id, the response was invalid so mark as failed if ( empty( $response->data->import_id ) ) { tribe( 'logger' )->log_debug( 'Response import ID was not provided.', 'EA Abstract' ); return $this->set_status_as_failed( tribe_error( 'core:aggregator:invalid-service-response' ) ); } // only set as pending if we aren't previewing the record if ( ! $is_previewing ) { // if we get here, we're good! Set the status to pending $this->set_status_as_pending(); } $service_supports_batch_push = ! empty( $response->batch_push ); /** * Whether batch pushing is supported for this record or not. * * @since 4.6.15 * * @param bool $service_supports_batch_push Whether the Service supports batch pushing or not. * @param Tribe__Events__Aggregator__Record__Abstract $this */ $allow_batch_push = apply_filters( 'tribe_aggregator_allow_batch_push', $service_supports_batch_push, $this ); if ( $allow_batch_push ) { $this->update_meta( 'allow_batch_push', true ); } // store the import id $this->update_meta( 'import_id', $response->data->import_id ); $this->should_queue_import( false ); return $response; } /** * Returns the record import data either fetching it locally or trying to retrieve * it from EA Service. * * @return stdClass|WP_Error An object containing the response data or a `WP_Error` on failure. */ public function get_import_data() { /** @var Tribe__Events__Aggregator $aggregator */ $aggregator = tribe( 'events-aggregator.main' ); $data = []; // For now only apply this to the URL type if ( 'url' === $this->type ) { $data = [ 'start' => $this->meta['start'], 'end' => $this->meta['end'], ]; } /** @var Tribe__Events__Aggregator__API__Import $import_api */ $import_api = $aggregator->api( 'import' ); if ( empty( $this->meta['import_id'] ) ) { return tribe_error( 'core:aggregator:record-not-finalized' ); } /** * Allow filtering of the Import data Request Args * * @since 4.6.18 * * @param array $data Which Arguments * @param Tribe__Events__Aggregator__Record__Abstract $record Record we are dealing with */ $data = apply_filters( 'tribe_aggregator_get_import_data_args', $data, $this ); $import_data = $import_api->get( $this->meta['import_id'], $data ); $import_data = $this->maybe_cast_to_error( $import_data ); return $import_data; } public function delete( $force = false ) { if ( $this->is_manual ) { return tribe_error( 'core:aggregator:delete-record-failed', [ 'record' => $this ], [ $this->id ] ); } return wp_delete_post( $this->id, $force ); } /** * Sets a status on the record * * @return int */ public function set_status( $status ) { if ( ! isset( Records::$status->{$status} ) ) { return false; } // Status of Scheduled Imports cannot change. if ( $this->post instanceof WP_Post && Records::$status->schedule === $this->post->post_status ) { return false; } $updated_id = wp_update_post( [ 'ID' => $this->id, 'post_status' => Records::$status->{$status}, ] ); if ( $updated_id !== $this->id || ! is_wp_error( $updated_id ) ) { // Reload the properties of the post if the status of the record was changed. $this->load( $this->id ); // If a parent exists and an error occur register the last update time on the parent record. if ( ! empty( $this->post->post_parent ) ) { $status = wp_update_post( [ 'ID' => $this->post->post_parent, 'post_modified' => Dates::build_date_object()->format( Dates::DBDATETIMEFORMAT ), ] ); } } return $status; } /** * Marks a record as failed * * @return int */ public function set_status_as_failed( $error = null ) { if ( $error && is_wp_error( $error ) ) { $this->log_error( $error ); } $this->set_status( 'failed' ); return $error; } /** * Marks a record as pending * * @return int */ public function set_status_as_pending() { return $this->set_status( 'pending' ); } /** * Marks a record as successful * * @return int */ public function set_status_as_success() { return $this->set_status( 'success' ); } /** * A quick method to fetch the Child Records to the current on this class * * @param array $args WP_Query Arguments * * @return WP_Query|WP_Error */ public function query_child_records( $args = [] ) { $defaults = []; $args = (object) wp_parse_args( $args, $defaults ); // Force the parent $args->post_parent = $this->id; return Records::instance()->query( $args ); } /** * A quick method to fetch the Child Records by Status * * @param string $status Which status, must be a valid EA status * * @return WP_Query|WP_Error|bool */ public function get_child_record_by_status( $status = 'success', $qty = -1, array $args = [] ) { $statuses = Records::$status; if ( ! isset( $statuses->{$status} ) && 'trash' !== $status ) { return false; } $args = array_merge( $args, [ 'post_status' => $statuses->{$status}, 'posts_per_page' => $qty, ] ); $query = $this->query_child_records( $args ); if ( ! $query->have_posts() ) { return false; } // Return the First Post when it exists return $query; } /** * Gets errors on the record post */ public function get_errors( $args = [] ) { $defaults = [ 'post_id' => $this->id, 'type' => Tribe__Events__Aggregator__Errors::$comment_type, ]; $args = wp_parse_args( $args, $defaults ); return get_comments( $args ); } /** * Logs an error to the comments of the Record post * * @param WP_Error $error Error message to log * * @return bool */ public function log_error( WP_Error $error ) { /** * Allow switching the logging of errors from EA off. * * Please dont turn this particular filter off without knowing what you are doing, it might cause problems and * will cause Support to likely be trying to help you without the information they might need. * * @since 5.12.1 * * @param bool $should_log_errors If we should log the errors or not. * @param WP_Error $error Which error we are logging. */ $should_log_errors = tribe_is_truthy( apply_filters( 'tec_aggregator_records_should_log_error', true, $error ) ); if ( ! $should_log_errors ) { return false; } $today = getdate(); $args = [ 'number' => 1, 'date_query' => [ [ 'year' => $today['year'], 'month' => $today['mon'], 'day' => $today['mday'], ], ], ]; // Tries To Fetch Comments for today $todays_errors = $this->get_errors( $args ); if ( ! empty( $todays_errors ) ) { return false; } $args = [ 'comment_post_ID' => $this->id, 'comment_author' => $error->get_error_code(), 'comment_content' => $error->get_error_message(), 'comment_type' => Tribe__Events__Aggregator__Errors::$comment_type, ]; return wp_insert_comment( $args ); } /** * Verifies if this Schedule Record can create a new Child Record * * @return boolean */ public function is_schedule_time() { if ( tribe_is_truthy( getenv( 'TRIBE_DEBUG_OVERRIDE_SCHEDULE' ) ) ) { return true; } // If we are not on a Schedule Type if ( ! $this->is_schedule ) { return false; } // If we are not dealing with the Record Schedule if ( Records::$status->schedule !== $this->post->post_status ) { return false; } // In some cases the scheduled import may be inactive and should not run during cron if ( false === $this->frequency ) { return false; } // It's never time for On Demand schedule, bail! if ( ! isset( $this->frequency->id ) || 'on_demand' === $this->frequency->id ) { return false; } $retry_interval = $this->get_retry_interval(); $failure_time_threshold = time() - $retry_interval; // If the last import status is an error and it happened before half the frequency ago let's try again if ( ( $this->has_own_last_import_status() && $this->failed_before( $failure_time_threshold ) ) || $this->last_child()->failed_before( $failure_time_threshold ) ) { return true; } $current = time(); $last = strtotime( $this->post->post_modified_gmt ); $next = $last + $this->frequency->interval; // let's add some randomization of -5 to 0 minutes (this makes sure we don't push a schedule beyond when it should fire off) $next += ( mt_rand( -5, 0 ) * 60 ); // Only do anything if we have one of these metas if ( ! empty( $this->meta['schedule_day'] ) || ! empty( $this->meta['schedule_time'] ) ) { // Setup to avoid notices $maybe_next = 0; // Now depending on the type of frequency we build the switch ( $this->frequency->id ) { case 'daily': $time_string = date( 'Y-m-d' ) . ' ' . $this->meta['schedule_time']; $maybe_next = strtotime( $time_string ); break; case 'weekly': $start_week = date( 'Y-m-d', strtotime( '-' . date( 'w' ) . ' days' ) ); $scheduled_day = date( 'Y-m-d', strtotime( $start_week . ' +' . ( (int) $this->meta['schedule_day'] - 1 ) . ' days' ) ); $time_string = date( 'Y-m-d', strtotime( $scheduled_day ) ) . ' ' . $this->meta['schedule_time']; $maybe_next = strtotime( $time_string ); break; case 'monthly': $time_string = date( 'Y-m-' ) . $this->meta['schedule_day'] . ' ' . $this->meta['schedule_time']; $maybe_next = strtotime( $time_string ); break; } // If our Next date based on Last run is bigger than the scheduled time it means we bail if ( $maybe_next > $next ) { $next = $maybe_next; } } return $current > $next; } /** * Verifies if this Record can pruned * * @return boolean */ public function has_passed_retention_time() { // Bail if we are trying to prune a Schedule Record if ( Records::$status->schedule === $this->post->post_status ) { return false; } $current = time(); $created = strtotime( $this->post->post_date_gmt ); // Prevents Pending that is younger than 1 hour to be pruned if ( Records::$status->pending === $this->post->post_status && $current < $created + HOUR_IN_SECONDS ) { return false; } $prune = $created + Records::instance()->get_retention(); return $current > $prune; } /** * Get info about the source, via and title * * @return array */ public function get_source_info() { if ( in_array( $this->origin, [ 'ics', 'csv' ] ) ) { if ( empty( $this->meta['source_name'] ) ) { $file = get_post( $this->meta['file'] ); $title = $file instanceof WP_Post ? $file->post_title : sprintf( esc_html__( 'Deleted Attachment: %d', 'the-events-calendar' ), $this->meta['file'] ); } else { $title = $this->meta['source_name']; } $via = $this->get_label(); } else { if ( empty( $this->meta['source_name'] ) ) { $title = $this->meta['source']; } else { $title = $this->meta['source_name']; } $via = $this->get_label(); if ( in_array( $this->origin, [ 'meetup' ] ) ) { $via = '' . esc_html( $via ) . '' . __( ' (opens in a new window)', 'the-events-calendar' ) . ''; } } return [ 'title' => $title, 'via' => $via ]; } /** * Fetches the status message for the last import attempt on (scheduled) records * * @param string $type Type of message to fetch * @param bool $lookup_children Whether the function should try to read the last children post status to return a coherent * last import status or not, default `false`. * * @return bool|string Either the message corresponding to the last import status or `false` if the last import status * is empty or not the one required. */ public function get_last_import_status( $type = 'error', $lookup_children = false ) { $status = $this->has_own_last_import_status() ? $this->meta['last_import_status'] : null; if ( empty( $status ) && $lookup_children ) { $last_child = $this->get_last_child_post(); if ( $last_child ) { $map = [ 'tribe-ea-failed' => 'error:import-failed', 'tribe-ea-success' => 'success:queued', ]; $status = Tribe__Utils__Array::get( $map, $last_child->post_status, null ); } } if ( ! $status ) { return false; } if ( 0 !== strpos( $status, $type ) ) { return false; } if ( 'error:usage-limit-exceeded' === $status ) { return __( 'When this import was last scheduled to run, the daily limit for your Event Aggregator license had already been reached.', 'the-events-calendar' ); } return tribe( 'events-aggregator.service' )->get_service_message( $status ); } /** * Updates the source name on the import record and its parent (if the parent exists) * * @param string $source_name Source name to set on the import record */ public function update_source_name( $source_name ) { // if we haven't received a source name, bail if ( empty( $source_name ) ) { return; } $this->update_meta( 'source_name', $source_name ); if ( empty( $this->post->post_parent ) ) { return; } $parent_record = Records::instance()->get_by_post_id( $this->post->post_parent ); if ( tribe_is_error( $parent_record ) ) { return; } $parent_record->update_meta( 'source_name', $source_name ); } /** * Queues events, venues, and organizers for insertion * * @param array $data Import data. * @param bool $start_immediately Whether the data processing should start immediately or not. * * @return array|Tribe__Events__Aggregator__Record__Queue_Interface|WP_Error|Tribe__Events__Aggregator__Record__Activity */ public function process_posts( $data = [], $start_immediately = false ) { if ( ! $start_immediately && 'manual' === $this->type ) { /** @var Tribe__Events__Aggregator__Service $service */ $service = tribe( 'events-aggregator.service' ); $service->confirm_import( $this->meta ); } // CSV should be processed right away as does not have support for batch pushing. $is_not_csv = empty( $data ) || empty( $data['origin'] ) || 'csv' !== $data['origin']; // if this is a batch push record then set its queue to fetching // to feed the UI something coherent if ( $is_not_csv && ! $this->is_polling() ) { // @todo let's revisit this to return when more UI is exposed $queue = new Batch_Queue( $this ); if ( $start_immediately ) { $queue->process(); return $queue->activity(); } return $queue; } $items = $this->prep_import_data( $data ); if ( is_wp_error( $items ) ) { tribe( 'logger' )->log_debug( 'Error while preparing the items of the request.', 'EA Process Posts.' ); $this->set_status_as_failed( $items ); return $items; } $queue = Tribe__Events__Aggregator__Record__Queue_Processor::build_queue( $this, $items ); if ( $start_immediately && is_array( $items ) ) { $queue->process(); } return $queue->activity(); } /** * Returns whether or not the record has a queue * * @return bool */ public function has_queue() { return ! empty( $this->meta[ Tribe__Events__Aggregator__Record__Queue::$queue_key ] ); } public function get_event_count( $type = null ) { if ( $type === null ) { return 0; } if ( empty( $this->meta['activity'] ) || ! $this->meta['activity'] instanceof Tribe__Events__Aggregator__Record__Activity ) { return 0; } $activity_type = 'event'; if ( ! empty( $this->meta['content_type'] ) ) { $activity_type = $this->meta['content_type']; } switch ( $type ) { case 'total': return $this->meta['activity']->count( $activity_type, 'created' ) + $this->meta['activity']->count( $activity_type, 'updated' ); default: return $this->meta['activity']->count( $activity_type, $type ); } } /** * Handles import data before queuing * * Ensures the import record source name is accurate, checks for errors, and limits import items * based on selection * * @param array $data Import data * * @return array|WP_Error */ public function prep_import_data( $data = [] ) { if ( empty( $data ) ) { $data = $this->get_import_data(); } if ( is_wp_error( $data ) ) { tribe( 'logger' )->log_debug( 'Data of the import has errors.', 'EA Prepare Import' ); $this->set_status_as_failed( $data ); return $data; } $this->update_source_name( empty( $data->data->source_name ) ? null : $data->data->source_name ); if ( empty( $this->meta['finalized'] ) ) { return tribe_error( 'core:aggregator:record-not-finalized' ); } if ( ! isset( $data->data->events ) ) { return 'fetch'; } return $this->filter_data_by_selected( $data->data->events ); } /** * Inserts events, venues, and organizers for the Import Record * * @param array $items Dummy data var to allow children to optionally react to passed in data * * @return Tribe__Events__Aggregator__Record__Activity The import activity record. */ public function insert_posts( $items = [] ) { add_filter( 'tribe-post-origin', [ Records::instance(), 'filter_post_origin' ], 10 ); /** * Fires before events and linked posts are inserted in the database. * * @since 4.5.13 * * @param array $items An array of items to insert. * @param array $meta The record meta information. */ do_action( 'tribe_aggregator_before_insert_posts', $items, $this->meta ); // sets the default user ID to that of the first user that can edit events $default_user_id = $this->get_default_user_id(); // Creates an Activity to log what Happened $activity = new Tribe__Events__Aggregator__Record__Activity(); $initial_created_events = $activity->count( Tribe__Events__Main::POSTTYPE ); $expected_created_events = $initial_created_events + count( $items ); $unique_field = $this->get_unique_field(); $existing_ids = $this->get_existing_ids_from_import_data( $items ); // cache $possible_parents = []; $found_organizers = []; $found_venues = []; $origin = $this->meta['origin']; $show_map_setting = tribe_is_truthy( tribe( 'events-aggregator.settings' )->default_map( $origin ) ); $update_authority_setting = tribe( 'events-aggregator.settings' )->default_update_authority( $origin ); $import_settings = tribe( 'events-aggregator.settings' )->default_settings_import( $origin ); $should_import_settings = tribe_is_truthy( $import_settings ) ? true : false; $args = [ 'post_status' => tribe( 'events-aggregator.settings' )->default_post_status( $origin ), ]; if ( ! empty( $this->meta['post_status'] ) && 'do_not_override' !== $this->meta['post_status'] ) { $args['post_status'] = $this->meta['post_status']; } /** * When an event/venue/organizer is being updated/inserted in the context of an import then any change * should not be tracked as if made by the user. So doing would result results in posts * "locked", under the "Import events but preserve local changes to event fields" event * authority, after an update/insertion. */ add_filter( 'tribe_tracker_enabled', '__return_false' ); foreach ( $items as $item ) { $event = Tribe__Events__Aggregator__Event::translate_service_data( $item ); // Configure the Post Type (enforcing) $event['post_type'] = Tribe__Events__Main::POSTTYPE; // Set the event ID if it can be set if ( $this->origin !== 'url' && $unique_field && isset( $event[ $unique_field['target'] ] ) && isset( $existing_ids[ $event[ $unique_field['target'] ] ] ) ) { $event_post_id = $existing_ids[ $event[ $unique_field['target'] ] ]->post_id; if ( tribe_is_event( $event_post_id ) ) { $event['ID'] = $event_post_id; } } // Checks if we need to search for Global ID if ( ! empty( $item->global_id ) ) { $global_event = Tribe__Events__Aggregator__Event::get_post_by_meta( 'global_id', $item->global_id ); // If we found something we will only update that Post if ( $global_event ) { $event['ID'] = $global_event->ID; } } // Only set the post status if there isn't an ID if ( empty( $event['ID'] ) ) { $event['post_status'] = Tribe__Utils__Array::get( $args, 'post_status', $this->meta['post_status'] ); /** * Allows services to provide their own filtering of event post statuses before import, especially * to handle the (do not override) status. * * @since 4.8.2 * * @param string $post_status The event's post status before being filtered. * @param array $event The WP event data about to imported and saved to the DB. * @param Tribe__Events__Aggregator__Record__Abstract $record The import's EA Import Record. */ $event['post_status'] = apply_filters( 'tribe_aggregator_new_event_post_status_before_import', $event['post_status'], $event, $this ); } /** * Should events that have previously been imported be overwritten? * * By default this is turned off (since it would reset the post status, description * and any other fields that have subsequently been edited) but it can be enabled * by returning true on this filter. * * @var bool $overwrite * @var int $event_id */ if ( ! empty( $event['ID'] ) && 'retain' === $update_authority_setting ) { // Log this Event was Skipped $activity->add( 'event', 'skipped', $event['ID'] ); continue; } if ( $show_map_setting ) { $event['EventShowMap'] = $show_map_setting || (bool) isset( $event['show_map'] ); if ( $this->has_import_policy_for( $origin, 'show_map_link' ) ) { $event['EventShowMapLink'] = isset( $event['show_map_link'] ) ? (bool) $event['show_map_link'] : $show_map_setting; } else { $event['EventShowMapLink'] = $show_map_setting; } } unset( $event['show_map'], $event['show_map_link'] ); if ( $should_import_settings && isset( $event['hide_from_listings'] ) ) { if ( $event['hide_from_listings'] == true ) { $event['EventHideFromUpcoming'] = 'yes'; } unset( $event['hide_from_listings'] ); } if ( $should_import_settings && isset( $event['sticky'] ) ) { if ( $event['sticky'] == true ) { $event['EventShowInCalendar'] = 'yes'; $event['menu_order'] = -1; } unset( $event['sticky'] ); } if ( ! $should_import_settings ) { unset( $event['feature_event'] ); } // set the parent if ( ! empty( $event['ID'] ) && ( $id = wp_get_post_parent_id( $event['ID'] ) ) ) { $event['post_parent'] = $id; } elseif ( ! empty( $event['parent_uid'] ) && ( $k = array_search( $event['parent_uid'], $possible_parents ) ) ) { $event['post_parent'] = $k; } // Do we have an existing venue for this event that we should preserve? // @todo [BTRIA-588]: Review - should we care about the potential for multiple venue IDs? if ( ! empty( $event['ID'] ) && 'preserve_changes' === $update_authority_setting && $existing_venue_id = tribe_get_venue_id( $event['ID'] ) ) { $event['EventVenueID'] = $existing_venue_id; unset( $event['Venue'] ); } // if we should create a venue or use existing if ( ! empty( $event['Venue']['Venue'] ) ) { $event['Venue']['Venue'] = trim( $event['Venue']['Venue'] ); $is_valid_origin = in_array( $this->origin, [ 'ics', 'csv', 'gcal', 'ical' ], true ); if ( ! empty( $item->venue->global_id ) || $is_valid_origin ) { // Pre-set for ICS based imports $venue = false; if ( ! empty( $item->venue->global_id ) ) { // Did we find a Post with a matching Global ID in History $venue = Tribe__Events__Aggregator__Event::get_post_by_meta( 'global_id_lineage', $item->venue->global_id ); } // Save the Venue Data for Updating $venue_data = $event['Venue']; if ( isset( $item->venue->description ) ) { $venue_data['Description'] = $item->venue->description; } if ( isset( $item->venue->excerpt ) ) { $venue_data['Excerpt'] = $item->venue->excerpt; } if ( isset( $item->venue->image ) ) { $venue_data['FeaturedImage'] = $item->venue->image; } if ( $venue ) { $venue_id = $event['EventVenueID'] = $venue_data['ID'] = $venue->ID; $found_venues[ $venue->ID ] = $event['Venue']['Venue']; // Here we might need to update the Venue depending on the main GlobalID if ( 'retain' === $update_authority_setting ) { // When we get here we say that we skipped an Venue $activity->add( 'venue', 'skipped', $venue->ID ); } else { if ( 'preserve_changes' === $update_authority_setting ) { $venue_data = Tribe__Events__Aggregator__Event::preserve_changed_fields( $venue_data ); } // Update the Venue Tribe__Events__Venue::instance()->update( $venue->ID, $venue_data ); // Tell that we updated the Venue to the activity tracker $activity->add( 'venue', 'updated', $venue->ID ); } } else { /** * Allows filtering the venue ID while searching for it. * * Use this filter to define custom ways to find a matching Venue provided the EA * record information; returning a non `null` value here will short-circuit the * check Event Aggregator would make. * * @since 4.6.15 * * @param int|null $venue_id The matching venue ID if any * @param array $venue The venue data from the record. */ $venue_id = apply_filters( 'tribe_aggregator_find_matching_venue', null, $event['Venue'] ); if ( null === $venue_id ) { // we search the venues already found in this request for this venue title $venue_id = array_search( $event['Venue']['Venue'], $found_venues ); } if ( ! $venue_id ) { $venue_unique_field = $this->get_unique_field( 'venue' ); /** * Whether Venues should be additionally searched by title when no match could be found * using other methods. * * @since 4.6.5 * * @param bool $lookup_venues_by_title * @param stdClass $item The event data that is being currently processed, it includes the Venue data * if any. * @param Tribe__Events__Aggregator__Record__Abstract $record The current record that is processing events. */ $lookup_venues_by_title = apply_filters( 'tribe_aggregator_lookup_venues_by_title', true, $item, $this ); if ( ! empty( $venue_unique_field ) ) { $target = $venue_unique_field['target']; $value = $venue_data[ $target ]; $venue = Tribe__Events__Aggregator__Event::get_post_by_meta( "_Venue{$target}", $value ); } if ( empty( $venue_unique_field ) || ( $lookup_venues_by_title && empty( $venue ) ) ) { $venue = get_page_by_title( $event['Venue']['Venue'], 'OBJECT', Tribe__Events__Venue::POSTTYPE ); } if ( $venue ) { $venue_id = $venue->ID; $found_venues[ $venue_id ] = $event['Venue']['Venue']; } } // We didn't find any matching Venue for the provided one if ( ! $venue_id ) { $event['Venue']['ShowMap'] = $show_map_setting; $event['Venue']['ShowMapLink'] = $show_map_setting; $venue_id = $event['EventVenueID'] = Tribe__Events__Venue::instance()->create( $event['Venue'], Tribe__Utils__Array::get( $event, 'post_status', $args['post_status'] ) ); $found_venues[ $event['EventVenueID'] ] = $event['Venue']['Venue']; // Log this Venue was created $activity->add( 'venue', 'created', $event['EventVenueID'] ); // Create the Venue Global ID if ( ! empty( $item->venue->global_id ) ) { update_post_meta( $event['EventVenueID'], Tribe__Events__Aggregator__Event::$global_id_key, $item->venue->global_id ); } // Create the Venue Global ID History if ( ! empty( $item->venue->global_id_lineage ) ) { foreach ( $item->venue->global_id_lineage as $gid ) { add_post_meta( $event['EventVenueID'], Tribe__Events__Aggregator__Event::$global_id_lineage_key, $gid ); } } } else { $event['EventVenueID'] = $venue_data['ID'] = $venue_id; // Here we might need to update the Venue depending we found something based on old code if ( 'retain' === $update_authority_setting ) { // When we get here we say that we skipped an Venue $activity->add( 'venue', 'skipped', $venue_id ); } else { if ( 'preserve_changes' === $update_authority_setting ) { $venue_data = Tribe__Events__Aggregator__Event::preserve_changed_fields( $venue_data ); } // Update the Venue Tribe__Events__Venue::instance()->update( $venue_id, $venue_data ); // Tell that we updated the Venue to the activity tracker $activity->add( 'venue', 'updated', $venue_id ); } } } } // Remove the Venue to avoid duplicates unset( $event['Venue'] ); } // Do we have an existing organizer(s) for this event that we should preserve? if ( ! empty( $event['ID'] ) && 'preserve_changes' === $update_authority_setting && $existing_organizer_ids = tribe_get_organizer_ids( $event['ID'] ) ) { $event['Organizer'] = $existing_organizer_ids; unset( $event['Organizer'] ); } if ( ! empty( $event['Organizer'] ) ) { $event_organizers = []; // make sure organizers is an array if ( $item->organizer instanceof stdClass ) { $item->organizer = [ $item->organizer ]; } foreach ( $event['Organizer'] as $key => $organizer_data ) { // if provided a valid Organizer ID right away use it if ( ! empty( $organizer_data['OrganizerID'] ) ) { if ( tribe_is_organizer( $organizer_data['OrganizerID'] ) ) { $event_organizers[] = (int) $organizer_data['OrganizerID']; continue; } unset( $organizer_data['OrganizerID'] ); } // if we should create an organizer or use existing if ( ! empty( $organizer_data['Organizer'] ) ) { $organizer_data['Organizer'] = trim( $organizer_data['Organizer'] ); if ( ! empty( $item->organizer[ $key ]->global_id ) || in_array( $this->origin, [ 'ics', 'ical', 'csv', 'gcal' ] ) ) { // Pre-set for ICS based imports $organizer = false; if ( ! empty( $item->organizer[ $key ]->global_id ) ) { // Did we find a Post with a matching Global ID in History $organizer = Tribe__Events__Aggregator__Event::get_post_by_meta( 'global_id_lineage', $item->organizer[ $key ]->global_id ); } if ( isset( $item->organizer[ $key ]->description ) ) { $organizer_data['Description'] = $item->organizer[ $key ]->description; } if ( isset( $item->organizer[ $key ]->excerpt ) ) { $organizer_data['Excerpt'] = $item->organizer[ $key ]->excerpt; } if ( $organizer ) { $organizer_id = $organizer_data['ID'] = $organizer->ID; $event_organizers[] = $organizer_id; // If we have a Image Field for the Organizers from Service if ( ! empty( $item->organizer[ $key ]->image ) ) { $this->import_organizer_image( $organizer_id, $item->organizer[ $key ]->image, $activity ); } $found_organizers[ $organizer->ID ] = $organizer_data['Organizer']; // Here we might need to update the Organizer depending we found something based on old code if ( 'retain' === $update_authority_setting ) { // When we get here we say that we skipped an Organizer $activity->add( 'organizer', 'skipped', $organizer->ID ); } else { if ( 'preserve_changes' === $update_authority_setting ) { $organizer_data = Tribe__Events__Aggregator__Event::preserve_changed_fields( $organizer_data ); } // Update the Organizer Tribe__Events__Organizer::instance()->update( $organizer->ID, $organizer_data ); // Tell that we updated the Organizer to the activity tracker $activity->add( 'organizer', 'updated', $organizer->ID ); } } else { /** * Allows filtering the organizer ID while searching for it. * * Use this filter to define custom ways to find a matching Organizer provided the EA * record information; returning a non `null` value here will short-circuit the * check Event Aggregator would make. * * @since 4.6.15 * * @param int|null $organizer_id The matching organizer ID if any * @param array $organizer The venue data from the record. */ $organizer_id = apply_filters( 'tribe_aggregator_find_matching_organizer', null, $organizer_data['Organizer'] ); if ( null === $organizer_id ) { // we search the organizers already found in this request for this organizer title $organizer_id = array_search( $organizer_data['Organizer'], $found_organizers ); } if ( ! $organizer_id ) { $organizer_unique_field = $this->get_unique_field( 'organizer' ); if ( ! empty( $organizer_unique_field ) ) { $target = $organizer_unique_field['target']; $value = $organizer_data[ $target ]; $organizer = Tribe__Events__Aggregator__Event::get_post_by_meta( "_Organizer{$target}", $value ); } else { $organizer = get_page_by_title( $organizer_data['Organizer'], 'OBJECT', Tribe__Events__Organizer::POSTTYPE ); } } if ( ! $organizer_id ) { if ( $organizer ) { $organizer_id = $organizer->ID; $found_organizers[ $organizer_id ] = $organizer_data['Organizer']; } } // We didn't find any matching Organizer for the provided one if ( ! $organizer_id ) { $organizer_id = $event_organizers[] = Tribe__Events__Organizer::instance()->create( $organizer_data, $event['post_status'] ); $found_organizers[ $organizer_id ] = $organizer_data['Organizer']; // Log this Organizer was created $activity->add( 'organizer', 'created', $organizer_id ); // Create the Organizer Global ID if ( ! empty( $item->organizer[ $key ]->global_id ) ) { update_post_meta( $organizer_id, Tribe__Events__Aggregator__Event::$global_id_key, $item->organizer[ $key ]->global_id ); } // Create the Organizer Global ID History if ( ! empty( $item->organizer[ $key ]->global_id_lineage ) ) { foreach ( $item->organizer[ $key ]->global_id_lineage as $gid ) { add_post_meta( $organizer_id, Tribe__Events__Aggregator__Event::$global_id_lineage_key, $gid ); } } } else { $event_organizers[] = $organizer_data['ID'] = $organizer_id; // Here we might need to update the Organizer depending we found something based on old code if ( 'retain' === $update_authority_setting ) { // When we get here we say that we skipped an Organizer $activity->add( 'organizer', 'skipped', $organizer_id ); } else { if ( 'preserve_changes' === $update_authority_setting ) { $organizer_data = Tribe__Events__Aggregator__Event::preserve_changed_fields( $organizer_data ); } // Update the Organizer Tribe__Events__Organizer::instance()->update( $organizer_id, $organizer_data ); // Tell that we updated the Organizer to the activity tracker $activity->add( 'organizer', 'updated', $organizer_id ); } } } } } } // Update the organizer submission data $event['Organizer']['OrganizerID'] = $event_organizers; // Let's remove this Organizer from the Event information if we found it if ( isset( $key ) && is_numeric( $key ) ) { unset( $event['Organizer'][ $key ] ); } } /** * Filters the event data before any sort of saving of the event * * @param array $event Event data to save * @param Tribe__Events__Aggregator__Record__Abstract Importer record */ $event = apply_filters( 'tribe_aggregator_before_save_event', $event, $this ); if ( ! empty( $event['ID'] ) ) { if ( 'preserve_changes' === $update_authority_setting ) { $event = Tribe__Events__Aggregator__Event::preserve_changed_fields( $event ); } add_filter( 'tribe_tracker_enabled', '__return_false' ); /** * Filters the event data before updating event * * @param array $event Event data to save * @param Tribe__Events__Aggregator__Record__Abstract Importer record */ $event = apply_filters( 'tribe_aggregator_before_update_event', $event, $this ); $event['ID'] = tribe_update_event( $event['ID'], $event ); remove_filter( 'tribe_tracker_enabled', '__return_false' ); // since the Event API only supports the _setting_ of these meta fields, we need to manually // delete them rather than relying on Tribe__Events__API::saveEventMeta() if ( isset( $event['EventShowMap'] ) && ! tribe_is_truthy( $event['EventShowMap'] ) ) { delete_post_meta( $event['ID'], '_EventShowMap' ); } if ( isset( $event['EventShowMapLink'] ) && ! tribe_is_truthy( $event['EventShowMapLink'] ) ) { delete_post_meta( $event['ID'], '_EventShowMapLink' ); } // Log that this event was updated $activity->add( 'event', 'updated', $event['ID'] ); } else { if ( 'url' !== $this->origin && isset( $event[ $unique_field['target'] ] ) ) { if ( isset( $existing_ids[ $event[ $unique_field['target'] ] ] ) ) { // we should not be here; probably a concurrency issue continue; } } // during cron runs the user will be set to 0; we assign the event to the first user that can edit events if ( ! isset( $event['post_author'] ) ) { $event['post_author'] = $default_user_id; } /** * Filters the event data before inserting event * * @param array $event Event data to save * @param Tribe__Events__Aggregator__Record__Abstract $record Importer record */ $event = apply_filters( 'tribe_aggregator_before_insert_event', $event, $this ); $event['ID'] = tribe_create_event( $event ); // Log this event was created $activity->add( 'event', 'created', $event['ID'] ); // Create the Event Global ID if ( ! empty( $item->global_id ) ) { update_post_meta( $event['ID'], Tribe__Events__Aggregator__Event::$global_id_key, $item->global_id ); } // Create the Event Global ID History if ( ! empty( $item->global_id_lineage ) ) { foreach ( $item->global_id_lineage as $gid ) { add_post_meta( $event['ID'], Tribe__Events__Aggregator__Event::$global_id_lineage_key, $gid ); } } } Records::instance()->add_record_to_event( $event['ID'], $this->id, $this->origin ); // Add post parent possibility if ( empty( $event['parent_uid'] ) && ! empty( $unique_field ) && ! empty( $event[ $unique_field['target'] ] ) ) { $possible_parents[ $event['ID'] ] = $event[ $unique_field['target'] ]; } // Save the unique field information if ( ! empty( $event[ $unique_field['target'] ] ) ) { update_post_meta( $event['ID'], "_{$unique_field['target']}", $event[ $unique_field['target'] ] ); } // Save the meta data in case of updating to pro later on if ( ! empty( $event['EventRecurrenceRRULE'] ) ) { update_post_meta( $event['ID'], '_EventRecurrenceRRULE', $event['EventRecurrenceRRULE'] ); } // Are there any existing event categories for this event? $terms = wp_get_object_terms( $event['ID'], Tribe__Events__Main::TAXONOMY ); if ( is_wp_error( $terms ) ) { $terms = []; } // If so, should we preserve those categories? if ( ! empty( $terms ) && 'preserve_changes' === $update_authority_setting ) { $terms = wp_list_pluck( $terms, 'term_id' ); unset( $event['categories'] ); } if ( ! empty( $event['categories'] ) ) { foreach ( $event['categories'] as $cat ) { if ( ! $term = term_exists( $cat, Tribe__Events__Main::TAXONOMY ) ) { $term = wp_insert_term( $cat, Tribe__Events__Main::TAXONOMY ); if ( ! is_wp_error( $term ) ) { $terms[] = (int) $term['term_id']; // Track that we created an event category $activity->add( 'cat', 'created', $term['term_id'] ); } } else { $terms[] = (int) $term['term_id']; } } } $tags = []; if ( ! empty( $event['tags'] ) ) { foreach ( $event['tags'] as $tag_name ) { if ( ! $tag = term_exists( $tag_name, 'post_tag' ) ) { $tag = wp_insert_term( $tag_name, 'post_tag' ); if ( ! is_wp_error( $tag ) ) { $tags[] = (int) $tag['term_id']; // Track that we created a post tag $activity->add( 'tag', 'created', $tag['term_id'] ); } } else { $tags[] = (int) $tag['term_id']; } } } // if we are setting all events to a category specified in saved import if ( ! empty( $this->meta['category'] ) ) { $terms[] = (int) $this->meta['category']; } $normalized_categories = tribe_normalize_terms_list( $terms, Tribe__Events__Main::TAXONOMY ); $normalized_tags = tribe_normalize_terms_list( $tags, 'post_tag' ); wp_set_object_terms( $event['ID'], $normalized_categories, Tribe__Events__Main::TAXONOMY, false ); wp_set_object_terms( $event['ID'], $normalized_tags, 'post_tag', false ); // If we have a Image Field from Service if ( ! empty( $event['image'] ) ) { $this->import_event_image( $event, $activity ); } // If we have a Image Field for the Venue from Service if ( ! empty( $item->venue->image ) && $venue_id ) { $this->import_venue_image( $venue_id, $item->venue->image, $activity ); } // update the existing IDs in the context of this batch if ( $unique_field && isset( $event[ $unique_field['target'] ] ) ) { $existing_ids[ $event[ $unique_field['target'] ] ] = (object) [ 'post_id' => $event['ID'], 'meta_value' => $event[ $unique_field['target'] ], ]; } /** * Fires after a single event has been created/updated, and with it its linked * posts, with import data. * * @since 4.6.16 * * @param array $event Which Event data was sent * @param array $item Raw version of the data sent from EA * @param self $record The record we are dealing with */ do_action( 'tribe_aggregator_after_insert_post', $event, $item, $this ); } remove_filter( 'tribe-post-origin', [ Records::instance(), 'filter_post_origin' ], 10 ); /** * Fires after events and linked posts have been inserted in the database. * * @since 4.5.13 * * @param array $items An array of items to insert. * @param array $meta The record meta information. * @param Tribe__Events__Aggregator__Record__Activity $activity The record insertion activity report. */ do_action( 'tribe_aggregator_after_insert_posts', $items, $this->meta, $activity ); /** * Finally resume tracking changes when all events, and linked posts, have been updated/inserted. */ remove_filter( 'tribe_tracker_enabled', '__return_false' ); $final_created_events = (int) $activity->count( Tribe__Events__Main::POSTTYPE ); if ( $expected_created_events === $final_created_events ) { $activity->set_last_status( Tribe__Events__Aggregator__Record__Activity::STATUS_SUCCESS ); } elseif ( $initial_created_events === $final_created_events ) { $activity->set_last_status( Tribe__Events__Aggregator__Record__Activity::STATUS_FAIL ); } else { $activity->set_last_status( Tribe__Events__Aggregator__Record__Activity::STATUS_PARTIAL ); } return $activity; } /** * Gets all ids that already exist in the post meta table from the provided records * * @param array $records Array of records * * @return array */ protected function get_existing_ids_from_import_data( $import_data ) { $unique_field = $this->get_unique_field(); if ( ! $unique_field ) { return []; } if ( ! empty( $this->meta['ids_to_import'] ) && 'all' !== $this->meta['ids_to_import'] ) { if ( is_array( $this->meta['ids_to_import'] ) ) { $selected_ids = $this->meta['ids_to_import']; } else { $selected_ids = json_decode( $this->meta['ids_to_import'] ); } } else { $source_field = $unique_field['source']; $selected_ids = array_filter( array_map( static function ( $entry ) use ( $source_field ) { $array_entry = (array) $entry; return $array_entry[ $source_field ] ?? null; }, $import_data ) ); } if ( empty( $selected_ids ) ) { return []; } $event_object = new Tribe__Events__Aggregator__Event; $existing_ids = $event_object->get_existing_ids( $this->meta['origin'], $selected_ids ); return $existing_ids; } protected function filter_data_by_selected( $import_data ) { $unique_field = $this->get_unique_field(); if ( ! $unique_field ) { return $import_data; } // It's safer to use Empty to check here, prevents notices if ( empty( $this->meta['ids_to_import'] ) ) { return $import_data; } if ( 'all' === $this->meta['ids_to_import'] ) { return $import_data; } $selected_ids = maybe_unserialize( $this->meta['ids_to_import'] ); $selected = []; foreach ( $import_data as $data ) { if ( ! in_array( $data->{$unique_field['source']}, $selected_ids ) ) { continue; } $selected[] = $data; } return $selected; } /** * Gets the unique field map for the current origin and the specified post type. * * @param string $for * * @return array|null */ protected function get_unique_field( $for = null ) { $fields = self::$unique_id_fields; switch ( $for ) { case 'venue': $fields = self::$unique_venue_id_fields; break; case 'organizer': $fields = self::$unique_organizer_id_fields; break; default: break; } if ( ! isset( $fields[ $this->meta['origin'] ] ) ) { return null; } return $fields[ $this->meta['origin'] ]; } /** * Finalizes the import record for insert */ public function finalize() { $this->update_meta( 'finalized', true ); /** * Fires after a record has been finalized and right before it starts importing. * * @since 4.6.21 * * @param int $id The Record post ID * @param array $meta An array of meta for the record * @param self $this The Record object itself */ do_action( 'tribe_aggregator_record_finalized', $this->id, $this->meta, $this ); } /** * preserve Event Options * * @param array $event Event data * * @return array */ public static function preserve_event_option_fields( $event ) { $event_post = get_post( $event['ID'] ); $post_meta = Tribe__Events__API::get_and_flatten_event_meta( $event['ID'] ); //preserve show map if ( isset( $post_meta['_EventShowMap'] ) && tribe_is_truthy( $post_meta['_EventShowMap'] ) ) { $event['EventShowMap'] = $post_meta['_EventShowMap']; } //preserve map link if ( isset( $post_meta['_EventShowMapLink'] ) && tribe_is_truthy( $post_meta['_EventShowMapLink'] ) ) { $event['EventShowMapLink'] = $post_meta['_EventShowMapLink']; } // we want to preserve this option if not explicitly being overridden if ( ! isset( $event['EventHideFromUpcoming'] ) && isset( $post_meta['_EventHideFromUpcoming'] ) ) { $event['EventHideFromUpcoming'] = $post_meta['_EventHideFromUpcoming']; } // we want to preserve the existing sticky state unless it is explicitly being overridden if ( ! isset( $event['EventShowInCalendar'] ) && '-1' == $event_post->menu_order ) { $event['EventShowInCalendar'] = 'yes'; } // we want to preserve the existing featured state unless it is explicitly being overridden if ( ! isset( $event['feature_event'] ) && isset( $post_meta['_tribe_featured'] ) ) { $event['feature_event'] = $post_meta['_tribe_featured']; } return $event; } /** * Imports an image information from EA server and creates the WP attachment object if required. * * @param array $event An event representation in the format provided by an Event Aggregator response. * * @return bool|stdClass|WP_Error An image information in the format provided by an Event Aggregator responsr or * `false` on failure. */ public function import_aggregator_image( $event ) { // Attempt to grab the event image $image_import = tribe( 'events-aggregator.main' )->api( 'image' )->get( $event['image']->id, $this ); /** * Filters the returned event image url * * @param array|bool $image Attachment information * @param array $event Event array */ $image = apply_filters( 'tribe_aggregator_event_image', $image_import, $event ); // If there was a problem bail out if ( false === $image ) { return false; } // Verify for more Complex Errors if ( is_wp_error( $image ) ) { return $image; } return $image; } /** * Imports the image contained in the post data `image` field if any. * * @param array $data A post data in array format. * * @return object|bool An object with the image post ID or `false` on failure. */ public function import_image( $data ) { if ( empty( $data['image'] ) || ! ( filter_var( $data['image'], FILTER_VALIDATE_URL ) || filter_var( $data['image'], FILTER_VALIDATE_INT ) ) ) { return false; } $uploader = new Tribe__Image__Uploader( $data['image'] ); $thumbnail_id = $uploader->upload_and_get_attachment_id(); return false !== $thumbnail_id ? (object) [ 'post_id' => $thumbnail_id ] : false; } /** * Whether an origin has more granulat policies concerning an import setting or not. * * @param string $origin * @param string $setting * * @return bool */ protected function has_import_policy_for( $origin, $setting ) { return isset( $this->origin_import_policies[ $origin ] ) && in_array( $setting, $this->origin_import_policies[ $origin ] ); } /** * Starts monitoring the db for errors. */ protected function watch_for_db_errors() { /** @var wpdb $wpdb */ global $wpdb; $this->last_wpdb_error = $wpdb->last_error; } /** * @return bool Whether a db error happened during the insertion of data or not. */ protected function db_errors_happened() { /** @var wpdb $wpdb */ global $wpdb; return $wpdb->last_error !== $this->last_wpdb_error; } /** * Cast error responses from the Service to WP_Errors to ease processing down the line. * * If a response is a WP_Error already or is not an error response then it will not be modified. * * @since 4.5.9 * * @param WP_Error|object $import_data * * @return array|WP_Error */ protected function maybe_cast_to_error( $import_data ) { if ( is_wp_error( $import_data ) ) { return $import_data; } if ( ! empty( $import_data->status ) && 'error' === $import_data->status ) { $import_data = (array) $import_data; $code = Tribe__Utils__Array::get( $import_data, 'message_code', 'error:import-failed' ); /** @var Tribe__Events__Aggregator__Service $service */ $service = tribe( 'events-aggregator.service' ); $message = Tribe__Utils__Array::get( $import_data, 'message', $service->get_service_message( 'error:import-failed' ) ); $data = Tribe__Utils__Array::get( $import_data, 'data', [] ); $import_data = new WP_Error( $code, $message, $data ); } return $import_data; } /** * Sets the post associated with this record. * * @since 4.5.11 * * @param WP_post|int $post A post object or post ID */ public function set_post( $post ) { if ( ! $post instanceof WP_Post ) { $post = get_post( $post ); } $this->post = $post; } /** * Returns the user ID of the first user that can edit events or the current user ID if available. * * During cron runs current user ID will be set to 0; here we try to get a legit author user ID to * be used as an author using the first non-0 user ID among the record author, the current user, the * first available event editor. * * @since 4.5.11 * * @return int The user ID or `0` (not logged in user) if not possible. */ protected function get_default_user_id() { $post_type_object = get_post_type_object( Tribe__Events__Main::POSTTYPE ); // try the record author if ( ! empty( $this->post->post_author ) && user_can( $this->post->post_author, $post_type_object->cap->edit_posts ) ) { return $this->post->post_author; } // try the current user $current_user_id = get_current_user_id(); if ( ! empty( $current_user_id ) && current_user_can( $post_type_object->cap->edit_posts ) ) { return $current_user_id; } // let's try and find a legit author among the available event authors $authors = get_users( [ 'who' => 'authors' ] ); foreach ( $authors as $author ) { if ( user_can( $author, $post_type_object->cap->edit_posts ) ) { return $author->ID; } } return 0; } /** * Assigns a new post thumbnail to the specified post if needed. * * @since 4.5.13 * * @param int $post_id The ID of the post the thumbnail should be assigned to. * @param int $new_thumbnail_id The new attachment post ID. * * @return bool Whether the post thumbnail ID changed or not. */ protected function set_post_thumbnail( $post_id, $new_thumbnail_id ) { $current_thumbnail_id = has_post_thumbnail( $post_id ) ? (int) get_post_thumbnail_id( $post_id ) : false; if ( empty( $current_thumbnail_id ) || $current_thumbnail_id !== (int) $new_thumbnail_id ) { set_post_thumbnail( $post_id, $new_thumbnail_id ); return true; } return false; } /** * Getter/setter to check/set whether the import for this record should be queued on EA Service or not. * * Note this is a passive check: if the meta is not set or set to `false` we assume the import * should not be queued on EA Service. * * @since 4.6.2 * * @param bool $should_queue_import If a value is provided here then the `should_queue_import` meta will * be set to the boolean representation of that value. * * @return bool */ public function should_queue_import( $should_queue_import = null ) { $key = 'should_queue_import'; if ( null === $should_queue_import ) { return isset( $this->meta[ $key ] ) && true == $this->meta[ $key ]; } $this->update_meta( $key, (bool) $should_queue_import ); } /** * Attaches a service-provided image to an organizer. * * @since 4.6.9 * * @param int $organizer_id The organizer post ID. * @param string $image_url * @param Tribe__Events__Aggregator__Record__Activity $activity * * @return bool Whether the image was attached to the organizer or not. */ public function import_organizer_image( $organizer_id, $image_url, $activity ) { /** * Whether the organizer image should be imported and attached or not. * * @since 4.6.9 * * @param bool $import_organizer_image Defaults to `true` * @param int $organizer_id The organizer post ID * @param string $image_url The URL to the image that should be imported * @param Tribe__Events__Aggregator__Record__Activity $activity The importer activity so far */ $import_organizer_image = apply_filters( 'tribe_aggregator_import_organizer_image', true, $organizer_id, $image_url, $activity ); if ( ! $import_organizer_image ) { return false; } if ( ! tribe_is_organizer( $organizer_id ) ) { return false; } return $this->import_and_attach_image_to( $organizer_id, $image_url, $activity ); } /** * Attaches a service-provided image to a venue. * * @since 4.6.9 * * @param int $venue_id The venue post ID. * @param string $image_url * @param Tribe__Events__Aggregator__Record__Activity $activity * * @return bool Whether the image was attached to the venue or not. */ public function import_venue_image( $venue_id, $image_url, $activity ) { /** * Whether the venue image should be imported and attached or not. * * @since 4.6.9 * * @param bool $import_venue_image Defaults to `true` * @param int $venue_id The venue post ID * @param string $image_url The URL to the image that should be imported * @param Tribe__Events__Aggregator__Record__Activity $activity The importer activity so far */ $import_venue_image = apply_filters( 'tribe_aggregator_import_venue_image', true, $venue_id, $image_url, $activity ); if ( ! $import_venue_image ) { return false; } if ( ! tribe_is_venue( $venue_id ) ) { return false; } return $this->import_and_attach_image_to( $venue_id, $image_url, $activity ); } /** * Imports and attaches an image as post thumbnail to a post. * * @since 4.6.9 * * @param int $post_id * @param string $image_url * @param Tribe__Events__Aggregator__Record__Activity $activity * * @return bool `true` if the image was correctly downloaded and attached, `false` otherwise. */ protected function import_and_attach_image_to( $post_id, $image_url, $activity ) { $args = [ 'ID' => $post_id, 'image' => $image_url, 'post_title' => get_the_title( $post_id ), ]; $image = $this->import_image( $args ); if ( empty( $image ) ) { return false; } if ( is_wp_error( $image ) || empty( $image->post_id ) ) { return false; } // Set as featured image $image_attached = $this->set_post_thumbnail( $post_id, $image->post_id ); if ( $image_attached ) { // Log this attachment was created $activity->add( 'attachment', 'created', $image->post_id ); } return true; } /** * Attaches a service-provided image to an event. * * @since 4.6.9 * * @param array $event The event data. * @param Tribe__Events__Aggregator__Record__Activity $activity * * @return bool Whether the image was attached to the event or not. */ public function import_event_image( $event, $activity ) { // If this is not a valid event no need for additional work. if ( empty( $event['ID'] ) || ! tribe_is_event( $event['ID'] ) ) { return false; } /** * Whether the event image should be imported and attached or not. * * @since 4.6.9 * * @param bool $import_event_image Defaults to `true`. * @param array $event The event post ID. * @param Tribe__Events__Aggregator__Record__Activity $activity The importer activity so far. * * @return bool Either to import or not the image of the event. */ $import_event_image = apply_filters( 'tribe_aggregator_import_event_image', true, $event, $activity ); if ( ! $import_event_image ) { return false; } if ( is_object( $event['image'] ) ) { $image = $this->import_aggregator_image( $event ); } else { $image = $this->import_image( $event ); } if ( $image && ! is_wp_error( $image ) && ! empty( $image->post_id ) ) { // Set as featured image $featured_status = $this->set_post_thumbnail( $event['ID'], $image->post_id ); if ( $featured_status ) { // Log this attachment was created $activity->add( 'attachment', 'created', $image->post_id ); return true; } } return false; } /** * Returns this record last child record or the record itself if no children are found. * * @since 4.6.15 * * @return Tribe__Events__Aggregator__Record__Abstract */ public function last_child() { $last_child_post = $this->get_last_child_post(); return $last_child_post && $last_child_post instanceof WP_Post ? Records::instance()->get_by_post_id( $last_child_post->ID ) : $this; } /** * Returns this record last child post object. * * @since 4.6.15 * * @param bool $force Whether to use the the last child cached value or refetch it. * * @return WP_Post|false Either the last child post object or `false` on failure. */ public function get_last_child_post( $force = false ) { if ( $this->post->post_parent ) { return $this->post; } if ( ! $force && null !== $this->last_child ) { return $this->last_child; } $children_query_args = [ 'posts_per_page' => 1, 'order' => 'DESC', 'order_by' => 'modified' ]; if ( ! empty( $this->post ) && $this->post instanceof WP_Post ) { $children_query_args['post_parent'] = $this->post->ID; } $last_children_query = $this->query_child_records( $children_query_args ); if ( $last_children_query->have_posts() ) { return reset( $last_children_query->posts ); } return false; } /** * Whether this record failed before a specific time. * * @since 4.6.15 * * @param string|int $time A timestamp or a string parseable by the `strtotime` function. * * @return bool */ public function failed_before( $time ) { $last_import_status = $this->get_last_import_status( 'error', true ); if ( empty( $last_import_status ) ) { return false; } if ( ! is_numeric( $time ) ) { $time = strtotime( $time ); } return strtotime( $this->post->post_modified ) <= (int) $time; } /** * Whether the record has its own last import status stored in the meta or * it should be read from its last child record. * * @since 4.6.15 * * @return bool */ protected function has_own_last_import_status() { return ! empty( $this->meta['last_import_status'] ); } /** * Returns the default retry interval depending on this record frequency. * * @since 4.6.15 * * @return int */ public function get_retry_interval() { if ( $this->frequency->interval === DAY_IN_SECONDS ) { $retry_interval = 6 * HOUR_IN_SECONDS; } elseif ( $this->frequency->interval < DAY_IN_SECONDS ) { // do not retry and let the scheduled import try again next time $retry_interval = 0; } else { $retry_interval = DAY_IN_SECONDS; } /** * Filters the retry interval between a failure and a retry for a scheduled record. * * @since 4.6.15 * * @param int $retry_interval An interval in seconds; defaults to the record frequency / 2. * @param Tribe__Events__Aggregator__Record__Abstract $this */ return apply_filters( 'tribe_aggregator_scheduled_records_retry_interval', $retry_interval, $this ); } /** * Returns the record retry timestamp. * * @since 4.6.15 * * @return int|bool Either the record retry timestamp or `false` if the record will * not retry to import. */ public function get_retry_time() { $retry_interval = $this->get_retry_interval(); if ( empty( $retry_interval ) ) { return false; } if ( ! $this->get_last_import_status( 'error', true ) ) { return false; } $last_attempt_time = strtotime( $this->last_child()->post->post_modified_gmt ); $retry_time = $last_attempt_time + (int) $retry_interval; if ( $retry_time < time() ) { $retry_time = false; } /** * Filters the retry timestamp for a scheduled record. * * @since 4.6.15 * * @param int $retry_time A timestamp. * @param Tribe__Events__Aggregator__Record__Abstract $this */ return apply_filters( 'tribe_aggregator_scheduled_records_retry_interval', $retry_time, $this ); } /** * Whether the record will try to fetch the import data polling EA Service or * expecting batches of data being pushed to it by EA Service. * * @since 4.6.15 * * @return bool */ public function is_polling() { $is_polling = empty( $this->meta['allow_batch_push'] ) || ! tribe_is_truthy( $this->meta['allow_batch_push'] ); /** * Whether the current record is a Service polling one or not. * * @since 4.6.15 * * @param bool $is_polling * @param Tribe__Events__Aggregator__Record__Abstract $record */ return (bool) apply_filters( 'tribe_aggregator_record_is_polling', $is_polling, $this ); } /* * * Generates the hash that will be expected in the for the next batch of events. * * @since 4.6.15 * * @return string */ public function generate_next_batch_hash() { return md5( uniqid( '', true ) ); } } PKE  x Meetup.phpnu[ $meetup_api_key, ); $args = wp_parse_args( $args, $defaults ); return parent::queue_import( $args ); } /** * Gets the Regular Expression string to match a source URL * * @since 4.6.18 * * @return string */ public static function get_source_regexp() { return '^(https?:\/\/)?(www\.)?meetup\.com(\.[a-z]{2})?\/'; } /** * Public facing Label for this Origin * * @return string */ public function get_label() { return __( 'Meetup', 'the-events-calendar' ); } /** * Filters the event to ensure that a proper URL is in the EventURL * * @param array $event Event data * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return array */ public static function filter_event_to_force_url( $event, $record ) { if ( 'meetup' !== $record->origin ) { return $event; } if ( ! empty( $event['EventURL'] ) ) { return $event; } $event['EventURL'] = $record->meta['source']; return $event; } /** * Filters the event to ensure that fields are preserved that are not otherwise supported by Meetup * * @param array $event Event data * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return array */ public static function filter_event_to_preserve_fields( $event, $record ) { if ( 'meetup' !== $record->origin ) { return $event; } return self::preserve_event_option_fields( $event ); } /** * Returns the Meetup authorization token generation URL. * * @since 4.9.6 * * @param array $args * * @return string Either the URL to obtain Eventbrite authorization token or an empty string. */ public static function get_auth_url( $args = array() ) { $service = tribe( 'events-aggregator.service' ); $api = $service->api(); if ( $api instanceof WP_Error ) { return ''; } $key = $api->key; $key2 = null; if ( ! empty( $api->licenses['tribe-meetup'] ) ) { $meetup_license = $api->licenses['tribe-meetup']; if ( empty( $key ) ) { $key = $meetup_license; } else { $key2 = $meetup_license; } } $url = $api->domain . 'meetup/' . $key; $defaults = array( 'referral' => urlencode( home_url() ), 'admin_url' => urlencode( get_admin_url() ), 'type' => 'new', 'lang' => get_bloginfo( 'language' ), ); if ( $key2 ) { $defaults['licenses'] = array( 'tribe-meetup' => $key2, ); } $args = wp_parse_args( $args, $defaults ); $url = add_query_arg( $args, $url ); return $url; } } PKE mmList_Table.phpnu[ $screen, 'tab' => Tribe__Events__Aggregator__Tabs::instance()->get_active(), ]; $args = wp_parse_args( $args, $default ); parent::__construct( $args ); // Set Current Tab $this->tab = $args['tab']; // Set page Instance $this->page = Tribe__Events__Aggregator__Page::instance(); // Set current user $this->user = wp_get_current_user(); } /** * * @global array $avail_post_stati * @global WP_Query $wp_query * @global int $per_page * @global string $mode */ public function prepare_items() { if ( ! isset( $_GET['orderby'] ) ) { $_GET['orderby'] = 'imported'; } // Set order if ( isset( $_GET['order'] ) && 'asc' === $_GET['order'] ) { $order = 'asc'; } else { $order = 'desc'; } $args = [ 'post_type' => $this->screen->post_type, 'orderby' => 'modified', 'order' => $order, 'paged' => absint( isset( $_GET['paged'] ) ? $_GET['paged'] : 1 ), ]; $status = Tribe__Events__Aggregator__Records::$status; switch ( $this->tab->get_slug() ) { case 'scheduled': $args['ping_status'] = 'schedule'; $args['post_status'] = $status->schedule; break; case 'history': $args['post_status'] = [ $status->success, $status->failed, $status->pending, ]; break; } if ( isset( $_GET['origin'] ) ) { $args['post_mime_type'] = 'ea/' . $_GET['origin']; } // retrieve the "per_page" option $screen_option = $this->screen->get_option( 'per_page', 'option' ); // retrieve the value of the option stored for the current user $per_page = get_user_meta( $this->user->ID, $screen_option, true ); if ( empty ( $per_page ) || $per_page < 1 ) { // get the default value if none is set $per_page = $this->screen->get_option( 'per_page', 'default' ); } $args['posts_per_page'] = $per_page; $search_term = tribe_get_request_var( 's' ); if ( 'scheduled' === $this->tab->get_slug() && ! empty( $search_term ) ) { // nonce check if search form submitted. $nonce = isset( $_POST['s'] ) && isset( $_POST['aggregator']['nonce'] ) ? sanitize_text_field( $_POST['aggregator']['nonce'] ) : ''; if ( isset( $_GET['s'] ) || wp_verify_nonce( $nonce, 'aggregator_' . $this->tab->get_slug() . '_request' ) ) { $search_term = filter_var( $search_term, FILTER_VALIDATE_URL ) ? esc_url_raw( $search_term ) : sanitize_text_field( $search_term ); $args['meta_query'] = [ 'relation' => 'OR', [ 'key' => '_tribe_aggregator_source_name', 'value' => $search_term, 'compare' => 'LIKE', ], [ 'key' => '_tribe_aggregator_import_name', 'value' => $search_term, 'compare' => 'LIKE', ], [ 'key' => '_tribe_aggregator_source', 'value' => $search_term, 'compare' => 'LIKE', ], ]; } } $query = new WP_Query( $args ); $this->items = $query->posts; $this->set_pagination_args( [ 'total_items' => $query->found_posts, 'per_page' => $query->query_vars['posts_per_page'], ] ); } public function nonce() { wp_nonce_field( 'aggregator_' . $this->tab->get_slug() . '_request', 'aggregator[nonce]' ); } /** * Get a list of sortable columns. The format is: * 'internal-name' => 'orderby' * or * 'internal-name' => array( 'orderby', true ) * * The second format will make the initial sorting order be descending * * @return array */ protected function get_sortable_columns() { return [ 'imported' => 'imported', ]; } /** * @param string $which */ protected function extra_tablenav( $which ) { // Skip it early because we are not doing filters on MVP return false; if ( 'bottom' === $which ) { return false; } echo '
'; $field = (object) []; $field->label = esc_html__( 'Filter By Origin', 'the-events-calendar' ); $field->placeholder = esc_attr__( 'Filter By Origin', 'the-events-calendar' ); $field->options = tribe( 'events-aggregator.main' )->api( 'origins' )->get(); ?> tab->get_slug() ) { $field = (object) []; $field->label = esc_html__( 'Filter By Frequency', 'the-events-calendar' ); $field->placeholder = esc_attr__( 'Filter By Frequency', 'the-events-calendar' ); $field->options = Tribe__Events__Aggregator__Cron::instance()->get_frequency(); ?> '; } /** * Get an associative array ( option_name => option_title ) with the list * of bulk actions available on this table. * * * @return array */ protected function get_bulk_actions() { return [ [ 'id' => 'delete', 'text' => 'Delete', ], ]; } /** * Display the bulk actions dropdown. * * @param string $which The location of the bulk actions: 'top' or 'bottom'. * This is designated as optional for backwards-compatibility. */ protected function bulk_actions( $which = '' ) { // On History Tab there is no Bulk Actions if ( 'history' === $this->tab->get_slug() ) { return false; } if ( 'bottom' === $which ) { return false; } // disable bulk actions if the Aggregator service is inactive if ( ! tribe( 'events-aggregator.main' )->is_service_active() ) { return ''; } $field = (object) []; $field->label = esc_html__( 'Bulk Actions', 'the-events-calendar' ); $field->placeholder = esc_attr__( 'Bulk Actions', 'the-events-calendar' ); $field->options = $this->get_bulk_actions(); ?> link ) with the list * of views available on this table. * * @return array */ protected function get_views() { $views = []; $given_origin = isset( $_GET['origin'] ) ? $_GET['origin'] : false; $type = [ 'schedule' ]; if ( 'history' === $this->tab->get_slug() ) { $type[] = 'manual'; } $status = []; if ( 'history' === $this->tab->get_slug() ) { $status[] = 'success'; $status[] = 'failed'; $status[] = 'pending'; } else { $status[] = 'schedule'; } $origins = Tribe__Events__Aggregator__Records::instance()->count_by_origin( $type, $status ); $total = array_sum( $origins ); $link = $this->page->get_url( [ 'tab' => $this->tab->get_slug() ] ); $text = sprintf( _nx( 'All (%s)', 'All (%s)', $total, 'records' ), number_format_i18n( $total ) ); $views['all'] = ( $given_origin ? sprintf( '%s', $link, $text ) : $text ); foreach ( $origins as $origin => $count ) { $origin_instance = Tribe__Events__Aggregator__Records::instance()->get_by_origin( $origin ); if ( null === $origin_instance ) { $debug_message = sprintf( 'The aggregator origin "%s" contains records, but is not supported and was skipped in the counts.', $origin ); tribe( 'logger' )->log_debug( $debug_message, 'aggregator' ); continue; } $link = $this->page->get_url( [ 'tab' => $this->tab->get_slug(), 'origin' => $origin ] ); $text = $origin_instance->get_label() . sprintf( ' (%s)', number_format_i18n( $count ) ); $views[ $origin ] = ( $given_origin !== $origin ? sprintf( '%s', $link, $text ) : $text ); } return $views; } /** * * @return array */ public function get_columns() { $columns = []; switch ( $this->tab->get_slug() ) { case 'scheduled': // We only need the checkbox when the EA service is active because there aren't any bulk // actions when EA is disabled if ( tribe( 'events-aggregator.main' )->is_service_active() ) { $columns['cb'] = ''; } $columns['source'] = esc_html_x( 'Source', 'column name', 'the-events-calendar' ); $columns['frequency'] = esc_html_x( 'Frequency', 'column name', 'the-events-calendar' ); $columns['imported'] = esc_html_x( 'Last Import', 'column name', 'the-events-calendar' ); break; case 'history': $columns['source'] = esc_html_x( 'Source', 'column name', 'the-events-calendar' ); $columns['frequency'] = esc_html_x( 'Type', 'column name', 'the-events-calendar' ); $columns['imported'] = esc_html_x( 'When', 'column name', 'the-events-calendar' ); break; } $columns['total'] = esc_html_x( '# Imported', 'column name', 'the-events-calendar' ); /** * Filter the columns displayed in the Posts list table for a specific post type. * * @since 4.3 * * @param array $post_columns An array of column names. */ return apply_filters( 'tribe_aggregator_manage_record_columns', $columns ); } protected function handle_row_actions( $post, $column_name, $primary ) { if ( $primary !== $column_name ) { return ''; } if ( 'scheduled' !== $this->tab->get_slug() ) { return ''; } // disable row actions if the Aggregator service is inactive if ( ! tribe( 'events-aggregator.main' )->is_service_active() ) { return ''; } $post_type_object = get_post_type_object( $post->post_type ); $actions = []; if ( current_user_can( $post_type_object->cap->edit_post, $post->ID ) ) { $actions['edit'] = sprintf( '%s', get_edit_post_link( $post->ID ), __( 'Edit', 'the-events-calendar' ) ); $args = [ 'tab' => $this->tab->get_slug(), 'action' => 'run-import', 'ids' => absint( $post->ID ), 'nonce' => wp_create_nonce( 'aggregator_' . $this->tab->get_slug() . '_request' ), ]; $actions['run-now'] = sprintf( '%3$s', Tribe__Events__Aggregator__Page::instance()->get_url( $args ), esc_attr__( 'Start an import from this source now, regardless of schedule.', 'the-events-calendar' ), esc_html__( 'Run Import', 'the-events-calendar' ) ); } if ( current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) { $actions['delete'] = sprintf( '%s', get_delete_post_link( $post->ID, '', true ), __( 'Delete', 'the-events-calendar' ) ); } return $this->row_actions( $actions ); } /** * Returns the status icon HTML * * @param Tribe__Events__Aggregator__Record__Abstract $record * * @return array|string */ private function get_status_icon( $record ) { $post = $record->post; $classes[] = 'dashicons'; if ( false !== strpos( $post->post_status, 'tribe-ea-' ) ) { $classes[] = str_replace( 'tribe-ea-', 'tribe-ea-status-', $post->post_status ); } else { $classes[] = 'tribe-ea-status-' . $post->post_status; } $helper_text = ''; switch ( $post->post_status ) { case 'tribe-ea-success': $classes[] = 'dashicons-yes'; $helper_text = __( 'Import completed', 'the-events-calendar' ); break; case 'tribe-ea-failed': $classes[] = 'dashicons-warning'; $helper_text = __( 'Import failed', 'the-events-calendar' ); if ( $errors = $record->get_errors() ) { $error_messages = []; foreach ( $errors as $error ) { $error_messages[] = $error->comment_content; } $helper_text .= ': ' . implode( '; ', $error_messages ); } break; case 'tribe-ea-schedule': $classes[] = 'dashicons-backup'; $helper_text = __( 'Import schedule', 'the-events-calendar' ); break; case 'tribe-ea-pending': $classes[] = 'dashicons-clock'; $helper_text = __( 'Import pending', 'the-events-calendar' ); break; case 'tribe-ea-draft': $classes[] = 'dashicons-welcome-write-blog'; $helper_text = __( 'Import preview', 'the-events-calendar' ); break; } $html[] = '
'; $html[] = ''; $html[] = '
'; return $this->render( $html ); } private function render( $html = [], $glue = "\r\n", $echo = false ) { $html = implode( $glue, (array) $html ); if ( $echo ) { echo $html; } return $html; } public function column_source( $post ) { $record = Tribe__Events__Aggregator__Records::instance()->get_by_post_id( $post ); if ( tribe_is_error( $record ) ) { return ''; } if ( 'scheduled' !== $this->tab->get_slug() ) { $html[] = $this->get_status_icon( $record ); } $source_info = $record->get_source_info(); if ( is_array( $source_info['title'] ) ) { $source_info['title'] = implode( ', ', $source_info['title'] ); } $title = $source_info['title']; if ( ! empty( $record->meta['import_name'] ) ) { $title = $record->meta['import_name']; } if ( $record->is_schedule && tribe( 'events-aggregator.main' )->is_service_active() ) { $html[] = '

' . esc_html( $title ) . '

'; } else { $html[] = '

' . esc_html( $title ) . '

'; } $html[] = '

' . esc_html_x( 'via ', 'record via origin', 'the-events-calendar' ) . '' . $source_info['via'] . '

'; $html[] = ''; /** * Customize the Events > Import > History > Source column HTML. * * @since 5.1.1 * * @param array $html List of HTML details. * @param Tribe__Events__Aggregator__Record__Abstract $record The record object. */ $html = apply_filters( 'tribe_aggregator_manage_record_column_source_html', $html, $record ); return $this->render( $html ); } public function column_imported( $post ) { $html = []; $record = Tribe__Events__Aggregator__Records::instance()->get_by_post_id( $post ); if ( tribe_is_error( $record ) ) { return ''; } if ( 'scheduled' === $this->tab->get_slug() ) { $last_import_error = $record->get_last_import_status( 'error' ); $status = 'success'; if ( $last_import_error ) { $html[] = ''; $status = 'failed'; } $has_child_record = $record->get_child_record_by_status( $status, 1 ); if ( ! $has_child_record ) { $html[] = '' . esc_html__( 'Unknown', 'the-events-calendar' ) . ''; return $this->render( $html ); } } $last_import = null; $original = $post->post_modified_gmt; $time = strtotime( $original ); $now = current_time( 'timestamp', true ); $retry_time = false; if ( ! empty( $last_import_error ) ) { $retry_time = $record->get_retry_time(); } $html[] = ''; if ( ( $now - $time ) <= DAY_IN_SECONDS ) { $diff = human_time_diff( $time, $now ); if ( ( $now - $time ) > 0 ) { $html[] = sprintf( esc_html_x( 'about %s ago', 'human readable time ago', 'the-events-calendar' ), $diff ); } else { $html[] = sprintf( esc_html_x( 'in about %s', 'in human readable time', 'the-events-calendar' ), $diff ); } } else { $html[] = date( tribe_get_date_format( true ), $time ) . '
' . date( Tribe__Date_Utils::TIMEFORMAT, $time ); } $html[] = '
'; if ( $retry_time ) { $html[] = '
'; if ( ( $retry_time - $now ) <= DAY_IN_SECONDS ) { $diff = human_time_diff( $retry_time, $now ); $html[] = sprintf( esc_html_x( 'retrying in about %s', 'in human readable time', 'the-events-calendar' ), $diff ); } else { $html[] = sprintf( esc_html_x( 'retrying at %s', 'when the retry will happen, a date', 'the-events-calendar' ), date( tribe_get_date_format( true ), $retry_time ) ); } $html[] = '
'; } return $this->render( $html ); } public function column_frequency( $post ) { if ( 'schedule' === $post->ping_status ) { $frequency = Tribe__Events__Aggregator__Cron::instance()->get_frequency( [ 'id' => $post->post_content ] ); if ( ! empty( $frequency->text ) ) { $html[] = $frequency->text; } else { $html[] = esc_html__( 'Invalid Frequency', 'the-events-calendar' ); } } else { $html[] = esc_html__( 'One Time', 'the-events-calendar' ); } return $this->render( $html ); } public function column_total( $post ) { $html = []; $record = Tribe__Events__Aggregator__Records::instance()->get_by_post_id( $post ); if ( tribe_is_error( $record ) ) { return ''; } $last_imported = $record->get_child_record_by_status( 'success', 1 ); // is this the scheduled import page? if ( $last_imported && $last_imported->have_posts() ) { // Fetches the Record Object $last_imported = Tribe__Events__Aggregator__Records::instance()->get_by_post_id( $last_imported->post->ID ); if ( tribe_is_error( $last_imported ) ) { return ''; } $html[] = '
' . number_format_i18n( $record->get_event_count( 'created' ) ) . ' ' . esc_html__( 'all time', 'the-events-calendar' ) . '
'; $html[] = ''; $html[] = ''; } elseif ( 'schedule' === $record->type && ! empty( $record->post->post_parent ) ) { // is this a child of a schedule record on History page $created = $record->get_event_count( 'created' ); $html[] = number_format_i18n( $created ? $created : 0 ) . ' ' . esc_html__( 'new', 'the-events-calendar' ) . '
'; if ( ! empty( $record->post->post_parent ) && $updated = $record->get_event_count( 'updated' ) ) { $html[] = number_format_i18n( $updated ) . ' ' . esc_html__( 'updated', 'the-events-calendar' ) . '
'; } } else { // manual on History page $created = $record->get_event_count( 'created' ); $html[] = number_format_i18n( $created ? $created : 0 ) . ' ' . esc_html__( 'new', 'the-events-calendar' ) . '
'; if ( $updated = $record->get_event_count( 'updated' ) ) { $html[] = number_format_i18n( $updated ) . ' ' . esc_html__( 'updated', 'the-events-calendar' ) . '
'; } } return $this->render( $html, "\n" ); } /** * Handles the checkbox column output. * * @since 4.3.0 * @access public * * @param WP_Post $post The current WP_Post object. */ public function column_cb( $post ) { ?>
_pagination_args ) ) { return; } $total_items = $this->_pagination_args['total_items']; $total_pages = $this->_pagination_args['total_pages']; $infinite_scroll = false; if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { $infinite_scroll = $this->_pagination_args['infinite_scroll']; } if ( 'top' === $which && $total_pages > 1 ) { $this->screen->render_screen_reader_content( 'heading_pagination' ); } $output = '' . sprintf( /* translators: %s: Number of items. */ _n( '%s item', '%s items', $total_items, 'the-events-calendar' ), number_format_i18n( $total_items ) ) . ''; $current = $this->get_pagenum(); $removable_query_args = wp_removable_query_args(); $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( $_SERVER['REQUEST_URI'] ) : admin_url( $wp->request ); $current_url = set_url_scheme( $request_uri, 'relative' ); $current_url = remove_query_arg( $removable_query_args, $current_url ); $search_term = tribe_get_request_var( 's' ); if ( ! empty( $search_term ) ) { $search_term = filter_var( $search_term, FILTER_VALIDATE_URL ) ? esc_url_raw( $search_term ) : sanitize_text_field( $search_term ); $current_url = add_query_arg( 's', $search_term, $current_url ); } $page_links = []; $total_pages_before = ''; $total_pages_after = ''; $disable_first = 1 === $current || 2 === $current; $disable_last = $total_pages === $current || $total_pages - 1 === $current; $disable_prev = 1 === $current; $disable_next = $total_pages === $current; if ( $disable_first ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%s", esc_url( remove_query_arg( 'paged', $current_url ) ), __( 'First page', 'the-events-calendar' ), '«' ); } if ( $disable_prev ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%s", esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), __( 'Previous page', 'the-events-calendar' ), '‹' ); } if ( 'bottom' === $which ) { $html_current_page = $current; $total_pages_before = '' . __( 'Current Page', 'the-events-calendar' ) . ''; } else { $html_current_page = sprintf( "%s", '', $current, strlen( $total_pages ) ); } $html_total_pages = sprintf( "%s", number_format_i18n( $total_pages ) ); $page_links[] = $total_pages_before . sprintf( /* translators: 1: Current page, 2: Total pages. */ _x( '%1$s of %2$s', 'paging', 'the-events-calendar' ), $html_current_page, $html_total_pages ) . $total_pages_after; if ( $disable_next ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%s", esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), __( 'Next page', 'the-events-calendar' ), '›' ); } if ( $disable_last ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%s", esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), __( 'Last page', 'the-events-calendar' ), '»' ); } $pagination_links_class = 'pagination-links'; if ( ! empty( $infinite_scroll ) ) { $pagination_links_class .= ' hide-if-js'; } $output .= "\n" . join( "\n", $page_links ) . ''; if ( $total_pages ) { $page_class = $total_pages < 2 ? ' one-page' : ''; } else { $page_class = ' no-pages'; } ?>
get_by_post_id( $record ); } if ( ! is_object( $record ) || ! $record instanceof \Tribe__Events__Aggregator__Record__Abstract ) { $this->null_process = true; return; } $cleaner = $cleaner ? $cleaner : new Tribe__Events__Aggregator__Record__Queue_Cleaner(); $cleaner->remove_duplicate_pending_records_for( $record ); if ( $cleaner->maybe_fail_stalled_record( $record ) ) { $this->null_process = true; return; } $this->record = $record; $this->activity(); } /** * Get the activity if a call to a dynamic attribute is taking place in this case `$this->>activity` * * @since 5.3.0 * * @param string $key The dynamic key to be returned. * * @return mixed|Tribe__Events__Aggregator__Record__Activity */ public function __get( $key ) { if ( $key === 'activity' ) { return $this->activity(); } return null; } /** * Returns the activity object for the processing of this Queue. * * @since 5.3.0 * * @return mixed|Tribe__Events__Aggregator__Record__Activity */ public function activity() { if ( empty( $this->activity ) ) { if ( empty( $this->record->meta[ self::$activity_key ] ) || ! $this->record->meta[ self::$activity_key ] instanceof Tribe__Events__Aggregator__Record__Activity ) { $this->activity = new Tribe__Events__Aggregator__Record__Activity(); } else { $this->activity = $this->record->meta[ self::$activity_key ]; } } return $this->activity; } /** * Allows us to check if the Events Data has still pending * * @since 5.3.0 * * @return boolean */ public function is_fetching() { return $this->is_in_progress(); } /** * Shortcut to check how many items are going to be processed next. * * @since 5.3.0 * * @return int */ public function count() { return 0; } /** * Shortcut to check if this queue is empty or it has a null process. * * @since 5.3.0 * * @return boolean `true` if this queue instance has acquired the lock and * the count is 0, `false` otherwise. */ public function is_empty() { if ( $this->null_process ) { return true; } return ! $this->is_in_progress(); } /** * After the process has been completed make sure the `post_modified` and `post_status` are updated accordingly. * * @since 5.3.0 * * @return $this */ protected function complete() { // Updates the Modified time for the Record Log. $args = [ 'ID' => $this->record->post->ID, 'post_modified' => $this->now(), 'post_status' => Records::$status->success, ]; wp_update_post( $args ); return $this; } /** * Processes a batch for the queue * * @since 5.3.0 * * @throws Exception * * @param null $batch_size The batch size is ignored on batch import as is controlled via the initial filtered value. * * @return self|Tribe__Events__Aggregator__Record__Activity */ public function process( $batch_size = null ) { // This batch has not started yet, make sure to initiate this import. if ( empty( $this->record->meta['batch_started'] ) ) { $now = $this->now(); if ( ! $now instanceof DateTime ) { return $this; } $this->record->update_meta( 'batch_started', $now->format( Dates::DBDATETIMEFORMAT ) ); $this->record->update_meta( Tribe__Events__Aggregator__Record__Queue::$queue_key, 'fetch' ); $this->record->set_status_as_pending(); $this->start(); return $this; } if ( $this->record->post instanceof WP_Post && ! $this->is_in_progress() ) { return $this; } return $this->activity(); } /** * Get the current date time using UTC as the time zone. * * @since 5.3.0 * * @return DateTime|false|\Tribe\Utils\Date_I18n */ private function now() { return Dates::build_date_object( 'now', new DateTimeZone( 'UTC' ) ); } /** * Create the initial request to the EA server requesting that the client is ready to start getting batches of events. * * @since 5.3.0 */ public function start() { if ( empty( $this->record->meta['allow_batch_push'] ) || empty( $this->record->meta['import_id'] ) || empty( $this->record->meta['next_batch_hash'] ) ) { $error = new WP_Error( 'core:aggregator:invalid-batch-record', esc_html__( 'The batch registered does not have all the required fields.', 'the-events-calendar' ) ); $this->record->set_status_as_failed( $error ); return; } /** @var Tribe__Events__Aggregator__Service $service */ $service = tribe( 'events-aggregator.service' ); if ( $service->is_over_limit( true ) ) { $this->record->update_meta( 'last_import_status', 'error:usage-limit-exceeded' ); $this->record->set_status_as_failed(); return; } $version = $service->api['version']; $service->api['version'] = 'v2.0.0'; $body = [ 'batch_size' => $this->batch_size(), 'batch_interval' => $this->batch_interval(), 'tec_version' => TEC::VERSION, 'next_import_hash' => $this->record->meta['next_batch_hash'], 'api' => get_rest_url( get_current_blog_id(), 'tribe/event-aggregator/v1' ), ]; if ( isset( $this->record->meta['ids_to_import'] ) && is_array( $this->record->meta['ids_to_import'] ) ) { $body['selected_events'] = $this->record->meta['ids_to_import']; } $response = $service->post( "import/{$this->record->meta['import_id']}/deliver/", [ 'body' => $body ] ); if ( is_wp_error( $response ) ) { $this->record->set_status_as_failed( $response ); } $service->api['version'] = $version; } /** * Return the number of events delivered per batch. * * @since 5.3.0 * * @return int */ private function batch_size() { return (int) apply_filters( 'event_aggregator_event_batch_size', 10 ); } /** * Return the interval in seconds of the delivery of each batch. * * @since 5.3.0 * * @return int */ private function batch_interval() { return (int) apply_filters( 'event_aggregator_event_batch_interval', 10 ); } /** * Returns the total progress made on processing the queue so far as a percentage. * * @since 5.3.0 * * @return int */ public function progress_percentage() { if ( empty( $this->record ) ) { return 0; } if ( empty( $this->record->meta['total_events'] ) ) { // Backwards compatible if the total_events meta key is still not present. if ( empty( $this->record->meta['percentage_complete'] ) ) { return 0; } return (int) $this->record->meta['percentage_complete']; } $total = (int) $this->record->meta['total_events']; $done = (int) $this->record->activity()->count( TEC::POSTTYPE ); if ( 0 === $total ) { return 100; } return min( 100, max( 1, (int) ( 100 * ( $done / $total ) ) ) ); } /** * Sets a flag to indicate that update work is in progress for a specific event: * this can be useful to prevent collisions between cron-based updated and realtime * updates. * * The flag naturally expires after an hour to allow for recovery if for instance * execution hangs half way through the processing of a batch. * * @since 5.3.0 */ public function set_in_progress_flag() { // No operation. } /** * Clears the in progress flag. * * @since 5.3.0 */ public function clear_in_progress_flag() { // No operation. } /** * Indicates if the queue for the current event is actively being processed. * * @since 5.3.0 * * @return bool */ public function is_in_progress() { if ( empty( $this->record->id ) ) { return false; } if ( ! $this->record->post instanceof WP_Post ) { return false; } return $this->record->post->post_status === Records::$status->pending; } /** * Returns the primary post type the queue is processing * * @since 5.3.0 * * @return string */ public function get_queue_type() { $item_type = TEC::POSTTYPE; if ( ! empty( $this->record->origin ) && 'csv' === $this->record->origin ) { $item_type = $this->record->meta['content_type']; } return $item_type; } /** * Whether the current queue process is stuck or not. * * @since 5.3.0 * * @return bool */ public function is_stuck() { return false; } /** * Orderly closes the queue process. * * @since 5.3.0 * * @return bool */ public function kill_queue() { return true; } /** * Whether the current queue process failed or not. * * @since 5.3.0 * * @return bool */ public function has_errors() { return false; } /** * Returns the queue error message. * * @since 5.3.0 * * @return string */ public function get_error_message() { return ''; } } PKE [2-- Queue.phpnu[get_by_post_id( $record ); } if ( ! is_object( $record ) || ! $record instanceof \Tribe__Events__Aggregator__Record__Abstract ) { $this->null_process = true; return; } if ( is_wp_error( $items ) ) { $this->null_process = true; return; } $this->cleaner = $cleaner ? $cleaner : new Tribe__Events__Aggregator__Record__Queue_Cleaner(); $this->cleaner->remove_duplicate_pending_records_for( $record ); $failed = $this->cleaner->maybe_fail_stalled_record( $record ); if ( $failed ) { $this->null_process = true; return; } $this->record = $record; $this->activity(); if ( ! empty( $items ) ) { if ( 'fetch' === $items ) { $this->is_fetching = true; $this->items = 'fetch'; } else { $this->init_queue( $items ); } $this->save(); } else { $this->load_queue(); } $this->cleaner = $cleaner; } public function __get( $key ) { switch ( $key ) { case 'activity': return $this->activity(); break; } } protected function init_queue( $items ) { if ( 'csv' === $this->record->origin ) { $this->record->reset_tracking_options(); $this->importer = $items; $this->total = $this->importer->get_line_count(); $this->items = array_fill( 0, $this->total, true ); } else { $this->items = $items; // Count the Total of items now and stores as the total $this->total = count( $this->items ); } } protected function load_queue() { if ( empty( $this->record->meta[ self::$queue_key ] ) ) { $this->is_fetching = false; $this->items = []; } else { $this->items = $this->record->meta[ self::$queue_key ]; } if ( 'fetch' === $this->items ) { $this->is_fetching = true; } } public function activity() { if ( empty( $this->activity ) ) { if ( empty( $this->record->meta[ self::$activity_key ] ) || ! $this->record->meta[ self::$activity_key ] instanceof Tribe__Events__Aggregator__Record__Activity ) { $this->activity = new Tribe__Events__Aggregator__Record__Activity; } else { $this->activity = $this->record->meta[ self::$activity_key ]; } } return $this->activity; } /** * Allows us to check if the Events Data has still pending * * @return boolean */ public function is_fetching() { return $this->is_fetching; } /** * Shortcut to check how many items are going to be processed next * * @return int */ public function count() { return is_array( $this->items ) ? count( $this->items ) : 0; } /** * Shortcut to check if this queue is empty or it has a null process. * * @return boolean `true` if this queue instance has acquired the lock and * the count is 0, `false` otherwise. */ public function is_empty() { if ( $this->null_process ) { return true; } return $this->has_lock && 0 === $this->count(); } /** * Gets the queue's total * * @return int */ protected function get_total() { return $this->count() + $this->activity->count( $this->get_queue_type() ); } /** * Saves queue data to relevant meta keys on the post * * @return self */ protected function save() { $this->record->update_meta( self::$activity_key, $this->activity ); /** @var Tribe__Meta__Chunker $chunker */ $chunker = tribe( 'chunker' ); // this data has the potential to be very big, so we register it for possible chunking in the db $key = Tribe__Events__Aggregator__Record__Abstract::$meta_key_prefix . self::$queue_key; $chunker->register_chunking_for( $this->record->post->ID, $key ); if ( empty( $this->items ) ) { $this->record->delete_meta( self::$queue_key ); } else { $this->record->update_meta( self::$queue_key, $this->items ); } // If we have a parent also update that if ( ! empty( $this->record->post->post_parent ) ) { $parent = Tribe__Events__Aggregator__Records::instance()->get_by_post_id( $this->record->post->post_parent ); if ( ! tribe_is_error( $parent ) && isset( $parent->meta[ self::$activity_key ] ) ) { $activity = $parent->meta[ self::$activity_key ]; if ( $activity instanceof Tribe__Events__Aggregator__Record__Activity ) { $parent->update_meta( self::$activity_key, $activity->merge( $this->activity ) ); } } } // Updates the Modified time for the Record Log $args = [ 'ID' => $this->record->post->ID, 'post_modified' => date( Tribe__Date_Utils::DBDATETIMEFORMAT, current_time( 'timestamp' ) ), ]; if ( empty( $this->items ) ) { $args['post_status'] = Tribe__Events__Aggregator__Records::$status->success; } wp_update_post( $args ); $this->release_lock(); return $this; } /** * Processes a batch for the queue * * @return self|Tribe__Events__Aggregator__Record__Activity */ public function process( $batch_size = null ) { if ( $this->null_process ) { return $this; } $this->has_lock = $this->acquire_lock(); if ( $this->has_lock ) { if ( $this->is_fetching() ) { if ( $this->record->should_queue_import() ) { $response = $this->record->queue_import(); if ( $response instanceof WP_Error ) { // the import queueing generated an error $this->record->set_status_as_failed( $response ); return $this; } if ( is_numeric( $response ) ) { // the import queueing was rescheduled $this->record->set_status_as_pending(); return $this; } } $data = $this->record->prep_import_data(); if ( 'fetch' === $data || ! is_array( $data ) || is_wp_error( $data ) ) { $this->release_lock(); $this->is_fetching = false; return $this->activity(); } $this->init_queue( $data ); $this->save(); } if ( ! $batch_size ) { $batch_size = apply_filters( 'tribe_aggregator_batch_size', Tribe__Events__Aggregator__Record__Queue_Processor::$batch_size ); } /* * If the queue system is switched mid-imports this might happen. * In that case we conservatively stop (kill) the queue process. */ if ( ! is_array( $this->items ) ) { $this->kill_queue(); return $this; } // Every time we are about to process we reset the next var $this->next = array_splice( $this->items, 0, $batch_size ); if ( 'csv' === $this->record->origin ) { $activity = $this->record->continue_import(); } else { $activity = $this->record->insert_posts( $this->next ); } $this->activity = $this->activity()->merge( $activity ); } else { // this queue instance should not register any new activity $this->activity = $this->activity(); } return $this->save(); } /** * Returns the total progress made on processing the queue so far as a percentage. * * @return int */ public function progress_percentage() { if ( 0 === $this->count() ) { return 0; } $total = $this->get_total(); $processed = $total - $this->count(); $percent = ( $processed / $total ) * 100; return (int) $percent; } /** * Sets a flag to indicate that update work is in progress for a specific event: * this can be useful to prevent collisions between cron-based updated and realtime * updates. * * The flag naturally expires after an hour to allow for recovery if for instance * execution hangs half way through the processing of a batch. */ public function set_in_progress_flag() { if ( empty( $this->record->id ) ) { return; } Tribe__Post_Transient::instance()->set( $this->record->id, self::$in_progress_key, true, HOUR_IN_SECONDS ); } /** * Clears the in progress flag. */ public function clear_in_progress_flag() { if ( empty( $this->record->id ) ) { return; } Tribe__Post_Transient::instance()->delete( $this->record->id, self::$in_progress_key ); } /** * Indicates if the queue for the current event is actively being processed. * * @return bool */ public function is_in_progress() { if ( empty( $this->record->id ) ) { return false; } Tribe__Post_Transient::instance()->get( $this->record->id, self::$in_progress_key ); } /** * Returns the primary post type the queue is processing * * @return string */ public function get_queue_type() { $item_type = Tribe__Events__Main::POSTTYPE; if ( ! empty( $this->record->origin ) && 'csv' === $this->record->origin ) { $item_type = $this->record->meta['content_type']; } return $item_type; } /** * Acquires the global (db stored) queue lock if available. * * @since 4.5.12 * * @return bool Whether the lock could be acquired or not if another instance/process has * already acquired the lock. */ protected function acquire_lock() { if ( empty( $this->record->post->ID ) ) { return false; } $post_id = $this->record->post->ID; $post_transient = Tribe__Post_Transient::instance(); $locked = $post_transient->get( $post_id, 'aggregator_queue_lock' ); if ( ! empty( $locked ) ) { return false; } $post_transient->set( $post_id, 'aggregator_queue_lock', '1', 180 ); return true; } /** * Release the queue lock if this instance of the queue holds it. * * @since 4.5.12 * * @return bool */ protected function release_lock() { if ( empty( $this->record->post->ID ) || ! $this->has_lock ) { return false; } $post_id = $this->record->post->ID; $post_transient = Tribe__Post_Transient::instance(); $post_transient->delete( $post_id, 'aggregator_queue_lock' ); return true; } /** * Whether the current queue process is stuck or not. * * @since 4.6.21 * * @return mixed */ public function is_stuck() { return false; } /** * Orderly closes the queue process. * * @since 4.6.21 * * @return bool */ public function kill_queue() { return true; } /** * Whether the current queue process failed or not. * * @since 4.6.21 * * @return bool */ public function has_errors() { return false; } /** * Returns the queue error message. * * @since 4.6.21 * * @return string */ public function get_error_message() { return ''; } } PKE \Eventbrite.phpnu[ urlencode( site_url() ), ); $args = wp_parse_args( $args, $defaults ); return parent::queue_import( $args ); } /** * Gets the Regular Expression string to match a source URL * * @since 4.6.18 * * @return string */ public static function get_source_regexp() { return '^(https?:\/\/)?(www\.)?eventbrite\.[a-z]{2,3}(\.[a-z]{2})?\/'; } /** * Returns the Eventbrite authorization token generation URL. * * @param array $args * * @return string Either the URL to obtain Eventbrite authorization token or an empty string. */ public static function get_auth_url( $args = array() ) { $service = tribe( 'events-aggregator.service' ); if ( $service->api() instanceof WP_Error ) { return ''; } $api = $service->api(); $key = $api->key; $key2 = null; if ( ! empty( $api->licenses['tribe-eventbrite'] ) ) { $eb_license = $api->licenses['tribe-eventbrite']; if ( empty( $key ) ) { $key = $eb_license; } else { $key2 = $eb_license; } } $url = $service->api()->domain . 'eventbrite/' . $key; $defaults = array( 'referral' => urlencode( home_url() ), 'admin_url' => urlencode( get_admin_url() ), 'type' => 'new', 'lang' => get_bloginfo( 'language' ), ); if ( $key2 ) { $defaults['licenses'] = array( 'tribe-eventbrite' => $key2, ); } $args = wp_parse_args( $args, $defaults ); $url = add_query_arg( $args, $url ); return $url; } /** * Public facing Label for this Origin * * @return string */ public function get_label() { return __( 'Eventbrite', 'the-events-calendar' ); } /** * Filters the event to ensure that a proper URL is in the EventURL * * @param array $event Event data * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return array */ public static function filter_event_to_force_url( $event, $record ) { if ( 'eventbrite' !== $record->origin ) { return $event; } if ( ! empty( $event['EventURL'] ) ) { return $event; } $event['EventURL'] = $record->meta['source']; return $event; } /** * Filters the event to ensure that fields are preserved that are not otherwise supported by Eventbrite * * @param array $event Event data * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return array */ public static function filter_event_to_preserve_fields( $event, $record ) { if ( 'eventbrite' !== $record->origin ) { return $event; } return self::preserve_event_option_fields( $event ); } /** * Add Site URL for Eventbrite Requets * * @since 4.6.18 * * @param array $args EA REST arguments * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return mixed */ public static function filter_add_site_get_import_data( $args, $record ) { if ( 'eventbrite' !== $record->origin ) { return $args; } $args['site'] = urlencode( site_url() ); return $args; } /** * When "(do not override)" status option is used, this ensures the imported event's status matches its original Eventbrite.com status. * * @since 4.8.1 * * @param string $post_status The event's post status before being filtered. * @param array $event The WP event data about to imported and saved to the DB. * @param Tribe__Events__Aggregator__Record__Abstract $record The import's EA Import Record. * @return array */ public static function filter_setup_do_not_override_post_status( $post_status, $event, $record ) { // override status if set within import. $status = isset( $record->meta['post_status'] ) ? $record->meta['post_status'] : $post_status; if ( 'do_not_override' === $status ) { $status = 'publish'; if ( isset( $event['eventbrite']->status ) && 'draft' === $event['eventbrite']->status ) { $status = 'draft'; } // If not draft, looked if listed. If not, set to private. if ( 'draft' !== $status && isset( $event['eventbrite']->listed ) && ! tribe_is_truthy( $event['eventbrite']->listed ) ) { $status = 'private'; } } return $status; } /** * Helps to ensure that post status selection UIs always default to "(do not override)" for Eventbrite imports. * * @since 4.8.1 * * @return string The key for the "(do not override)" option. */ public static function filter_set_default_post_status() { return 'do_not_override'; } } PKE [$$gCal.phpnu[origin ) { return $event; } return self::preserve_event_option_fields( $event ); } } PKE  Void_Queue.phpnu[error = $error->get_error_message(); $this->wp_error = $error; return; } $this->error = $error; } /** * {@inheritdoc} */ public function activity() { return new Tribe__Events__Aggregator__Record__Activity(); } /** * {@inheritdoc} */ public function count() { return 0; } /** * {@inheritdoc} */ public function is_empty() { // A void queue is not empty, it's void. return false; } /** * {@inheritdoc} */ public function process( $batch_size = null ) { return $this; } /** * {@inheritdoc} */ public function progress_percentage() { // return a 0% progress percentage to make sure the queue processor will process it. return 0; } /** * {@inheritdoc} */ public function set_in_progress_flag() { // no-op } /** * {@inheritdoc} */ public function clear_in_progress_flag() { // no-op } /** * {@inheritdoc} */ public function is_in_progress() { // mark the queue as in progress to make the queue processor process it. return true; } /** * {@inheritdoc} */ public function get_queue_type() { // not really important, still let's maintain coherence. return Tribe__Events__Main::POSTTYPE; } /** * {@inheritdoc} */ public function is_stuck() { return true; } /** * {@inheritdoc} */ public function kill_queue() { if ( empty( $this->error ) ) { $this->error = __( 'Unable to process this import - a breakage or conflict may have resulted in the import halting.', 'the-events-calendar' ); } return true; } /** * {@inheritdoc} */ public function has_errors() { return null !== $this->error; } /** * {@inheritdoc} */ public function get_error_message() { return $this->error; } /** * Returns the `WP_Error` instance used to build this void queue, if any. * * @since 4.6.22 * * @return WP_Error|null The `WP_Error` used to build this void queue or `null` * if no `WP_Error` object was used to build this void queue. */ public function get_wp_error() { return $this->wp_error; } /** * This Queue never fetches on external resources so is always `false`. * * @return bool The state of the queue with external resources. */ public function is_fetching() { return false; } } PKE [M44CSV.phpnu[ empty( $this->meta['file'] ) ? null : $this->meta['file'], ); $meta = wp_parse_args( $meta, $defaults ); return parent::create( $type, $args, $meta ); } public function queue_import( $args = array() ) { $is_previewing = ( ! empty( $_GET['action'] ) && ( 'tribe_aggregator_create_import' === $_GET['action'] || 'tribe_aggregator_preview_import' === $_GET['action'] ) ); $data = $this->get_csv_data(); $result = array( 'status' => 'success', 'message_code' => 'success', 'data' => array( 'import_id' => $this->id, 'items' => $data, ), ); $first_row = reset( $data ); $columns = array_keys( $first_row ); $result['data']['columns'] = $columns; // store the import id update_post_meta( $this->id, self::$meta_key_prefix . 'import_id', $this->id ); // only set as pending if we aren't previewing the record if ( ! $is_previewing ) { // if we get here, we're good! Set the status to pending $this->set_status_as_pending(); } return $result; } /** * Public facing Label for this Origin * * @return string */ public function get_label() { return __( 'CSV', 'the-events-calendar' ); } public function get_csv_data() { if ( empty( $this->meta['file'] ) || ! $file_path = $this->get_file_path() ) { return $this->set_status_as_failed( tribe_error( 'core:aggregator:invalid-csv-file' ) ); } $content_type = str_replace( 'tribe_', '', $this->meta['content_type'] ); $file_reader = new Tribe__Events__Importer__File_Reader( $file_path ); $importer = Tribe__Events__Importer__File_Importer::get_importer( $content_type, $file_reader ); $this->update_meta( 'source_name', basename( $file_path ) ); $rows = $importer->do_import_preview(); /* * Strip whitespace from the beginning and end of row values */ $formatted_rows = array(); foreach ( $rows as $row ) { $formatted_rows[] = array_map( 'trim', $row ); } $rows = $formatted_rows; $headers = array_shift( $rows ); /* * To avoid empty columns from collapsing onto each other we provide * each column without an header a generated one. */ $empty_counter = 1; $formatted_headers = array(); foreach ( $headers as $header ) { if ( empty( $header ) ) { $header = __( 'Unknown Column ', 'the-events-calendar' ) . $empty_counter ++; } $formatted_headers[] = $header; } $headers = $formatted_headers; $data = array(); foreach ( $rows as $row ) { $item = array(); foreach ( $headers as $key => $header ) { $item[ $header ] = $row[ $key ]; } $data[] = $item; } return $data; } /** * Queues events, venues, and organizers for insertion * * @param array $data Import data * @param bool $ignored This parameter is, de facto, ignored when processing CSV files: all * imports are immediately started. * * @return array|WP_Error */ public function process_posts( $data = array(), $ignored = false ) { if ( 'csv' !== $data['origin'] || empty( $data['csv']['content_type'] ) ) { return tribe_error( 'core:aggregator:invalid-csv-parameters' ); } if ( $this->has_queue() ) { $queue = Tribe__Events__Aggregator__Record__Queue_Processor::build_queue( $this->post->ID ); return $queue->process(); } $importer = $this->prep_import_data( $data ); if ( tribe_is_error( $importer ) ) { return $importer; } $queue = Tribe__Events__Aggregator__Record__Queue_Processor::build_queue( $this->post->ID, $importer ); return $queue->process(); } /** * Handles import data before queuing * * Ensures the import record source name is accurate, checks for errors, and limits import items * based on selection * * @param array $data Import data * * @return array|WP_Error */ public function prep_import_data( $data = array() ) { if ( empty( $this->meta['finalized'] ) ) { return tribe_error( 'core:aggregator:record-not-finalized' ); } // if $data is an object already, don't attempt to manipulate it into an importer object if ( is_object( $data ) ) { return $data; } // if $data is empty, grab the data from meta if ( empty( $data ) ) { $data = $this->meta; } if ( empty( $data['column_map'] ) ) { return tribe_error( 'core:aggregator:missing-csv-column-map' ); } $content_type = $this->get_csv_content_type(); update_option( 'tribe_events_import_column_mapping_' . $content_type, $data['column_map'] ); try { $importer = $this->get_importer(); } catch ( RuntimeException $e ) { return tribe_error( 'core:aggregator:missing-csv-file' ); } if ( ! empty( $data['category'] ) ) { $importer = $this->maybe_set_default_category( $importer ); } if ( ! empty( $data['post_status'] ) ) { $importer = $this->maybe_set_default_post_status( $importer ); } $required_fields = $importer->get_required_fields(); $missing = array_diff( $required_fields, $data['column_map'] ); if ( ! empty( $missing ) ) { $mapper = new Tribe__Events__Importer__Column_Mapper( $content_type ); /** * @todo allow to overwrite the default message */ $message = '

' . esc_html__( 'The following fields are required for a successful import:', 'the-events-calendar' ) . '

'; $message .= '
    '; foreach ( $missing as $key ) { $message .= '
  • ' . $mapper->get_column_label( $key ) . '
  • '; } $message .= '
'; return new WP_Error( 'csv-invalid-column-mapping', $message ); } update_option( 'tribe_events_import_column_mapping_' . $content_type, $data['column_map'] ); return $importer; } public function get_importer() { if ( ! $this->importer ) { $content_type = $this->get_csv_content_type(); $file_path = $this->get_file_path(); $file_reader = new Tribe__Events__Importer__File_Reader( $file_path ); $this->importer = Tribe__Events__Importer__File_Importer::get_importer( $content_type, $file_reader ); $this->importer->set_map( get_option( 'tribe_events_import_column_mapping_' . $content_type, array() ) ); $this->importer->set_type( $content_type ); $this->importer->set_limit( absint( apply_filters( 'tribe_aggregator_batch_size', Tribe__Events__Aggregator__Record__Queue_Processor::$batch_size ) ) ); $this->importer->set_offset( 1 ); } return $this->importer; } public function get_content_type() { return str_replace( 'tribe_', '', $this->meta['content_type'] ); } /** * Translates the posttype-driven content types to content types that the CSV importer knows * * @param string $content_type Content Type * * @return string CSV Importer compatible content type */ public function get_csv_content_type( $content_type = null ) { if ( ! $content_type ) { $content_type = $this->get_content_type(); } $lowercase_content_type = strtolower( $content_type ); $map = array( 'event' => 'events', 'events' => 'events', 'organizer' => 'organizers', 'organizers' => 'organizers', 'venue' => 'venues', 'venues' => 'venues', ); if ( isset( $map[ $lowercase_content_type ] ) ) { return $map[ $lowercase_content_type ]; } return $content_type; } /** * Gets the available post types for importing * * @return array Array of Post Type Objects */ public function get_import_post_types() { $post_types = array( get_post_type_object( Tribe__Events__Main::POSTTYPE ), get_post_type_object( Tribe__Events__Organizer::POSTTYPE ), get_post_type_object( Tribe__Events__Venue::POSTTYPE ), ); /** * Filters the available CSV post types for the event aggregator form * * @param array $post_types Array of post type objects */ return apply_filters( 'tribe_aggregator_csv_post_types', $post_types ); } /** * Returns the path to the CSV file. * * @since 4.6.15 * * @return bool|false|string Either the absolute path to the CSV file or `false` on failure. */ protected function get_file_path() { if ( is_numeric( $this->meta['file'] ) ) { $file_path = get_attached_file( absint( $this->meta['file'] ) ); } else { $file_path = realpath( $this->meta['file'] ); } return $file_path && file_exists( $file_path ) ? $file_path : false; } private function begin_import() { $this->reset_tracking_options(); return $this->continue_import(); } public function reset_tracking_options() { update_option( 'tribe_events_importer_offset', 1 ); update_option( 'tribe_events_import_log', array( 'updated' => 0, 'created' => 0, 'skipped' => 0, 'encoding' => 0 ) ); update_option( 'tribe_events_import_failed_rows', array() ); update_option( 'tribe_events_import_encoded_rows', array() ); } public function continue_import() { $lock_key = 'tribe_ea_csv_import_' . $this->id; if ( ! $this->acquire_db_lock( $lock_key ) ) { return $this->meta['activity']; } $importer = $this->get_importer(); $importer->is_aggregator = true; $importer->aggregator_record = $this; $importer = $this->maybe_set_default_category( $importer ); $importer = $this->maybe_set_default_post_status( $importer ); $offset = (int) get_option( 'tribe_events_importer_offset', 1 ); if ( -1 === $offset ) { $this->state = 'complete'; $this->clean_up_after_import(); } else { $this->state = 'importing'; $importer->set_offset( $offset ); $this->do_import( $importer ); $this->log_import_results( $importer ); } return $this->meta['activity']; } /** * If a custom category has been specified, set it in the importer * * @param Tribe__Events__Importer__File_Importer $importer Importer object * * @return Tribe__Events__Importer__File_Importer */ public function maybe_set_default_category( $importer ) { if ( ! empty( $this->meta['category'] ) ) { $importer->default_category = (int) $this->meta['category']; } return $importer; } /** * If a custom post_status has been specified, set it in the importer * * @param Tribe__Events__Importer__File_Importer $importer Importer object * * @return Tribe__Events__Importer__File_Importer */ public function maybe_set_default_post_status( $importer ) { if ( ! empty( $this->meta['post_status'] ) ) { $importer->default_post_status = $this->meta['post_status']; } return $importer; } protected function do_import( Tribe__Events__Importer__File_Importer $importer ) { $importer->do_import(); $this->messages = $importer->get_log_messages(); $new_offset = $importer->import_complete() ? -1 : $importer->get_last_completed_row(); update_option( 'tribe_events_importer_offset', $new_offset ); $lock_key = 'tribe_ea_csv_import_' . $this->id; $this->release_db_lock( $lock_key ); if ( -1 === $new_offset ) { do_action( 'tribe_events_csv_import_complete' ); } } protected function log_import_results( Tribe__Events__Importer__File_Importer $importer ) { $log = get_option( 'tribe_events_import_log' ); if ( empty( $log['encoding'] ) ) { $log['encoding'] = 0; } $updated = $importer->get_updated_post_count(); $created = $importer->get_new_post_count(); $skipped = $importer->get_skipped_row_count(); if ( empty( $log['updated'] ) ) { $log['updated'] = 0; } if ( empty( $log['created'] ) ) { $log['created'] = 0; } if ( empty( $log['skipped'] ) ) { $log['skipped'] = 0; } if ( $updated ) { $this->meta['activity']->add( 'updated', $this->meta['content_type'], array_fill( 0, $updated, 1 ) ); } if ( $created ) { $this->meta['activity']->add( 'created', $this->meta['content_type'], array_fill( 0, $created, 1 ) ); } if ( $skipped ) { $this->meta['activity']->add( 'skipped', $this->meta['content_type'], array_fill( 0, $skipped, 1 ) ); } $log['updated'] += $updated; $log['created'] += $created; $log['skipped'] += $skipped; $log['encoding'] += $importer->get_encoding_changes_row_count(); update_option( 'tribe_events_import_log', $log ); $skipped_rows = $importer->get_skipped_row_numbers(); $previously_skipped_rows = get_option( 'tribe_events_import_failed_rows', array() ); $skipped_rows = $previously_skipped_rows + $skipped_rows; update_option( 'tribe_events_import_failed_rows', $skipped_rows ); $encoded_rows = $importer->get_encoding_changes_row_numbers(); $previously_encoded_rows = get_option( 'tribe_events_import_encoded_rows', array() ); $encoded_rows = $previously_encoded_rows + $encoded_rows; update_option( 'tribe_events_import_encoded_rows', $encoded_rows ); } private function clean_up_after_import() { Tribe__Events__Importer__File_Uploader::clear_old_files(); } } PKE הQueue_Interface.phpnu[record = $record; if ( empty( $this->record->meta['queue_id'] ) ) { $this->queue_process = $this->init_queue( $items ); } } /** * Initializes the async queue. * * @since 4.6.16 * * @param $items * * @return Tribe__Process__Queue|null Either a built and ready queue process or `null` * if the queue process was not built as not needed; * the latter will happen when there are no items to * process. */ protected function init_queue( $items ) { $items_initially_not_available = empty( $items ) || ! is_array( $items ); if ( $items_initially_not_available ) { $items = $this->record->prep_import_data(); } $items_still_not_available = empty( $items ) || ! is_array( $items ); if ( $items_still_not_available ) { if ( is_array( $items ) ) { /** * It means that there are actually no items to process. * No need to go further. */ $this->record->delete_meta( 'in_progress' ); $this->record->delete_meta( 'queue' ); $this->record->delete_meta( 'queue_id' ); $this->record->set_status_as_success(); } return null; } $transitional_id = $this->generate_transitional_id(); /** @var Tribe__Events__Aggregator__Record__Items $record_items */ $record_items = tribe( 'events-aggregator.record-items' ); $record_items->set_items( $items ); $items = $record_items->mark_dependencies()->get_items(); /** @var Tribe__Process__Queue $import_queue */ $import_queue = tribe( 'events-aggregator.processes.import-events' ); // Fetch and store the current blog ID to make sure each task knows the blog context it should happen into. $current_blog_id = is_multisite() ? get_current_blog_id() : 1; foreach ( $items as $item ) { $item_data = [ 'user_id' => get_current_user_id(), 'record_id' => $this->record->id, 'data' => $item, 'transitional_id' => $transitional_id, 'blog_id' => $current_blog_id, ]; $import_queue->push_to_queue( $item_data ); } $import_queue->save(); $queue_id = $import_queue->get_id(); $this->record->update_meta( 'queue_id', $queue_id ); $this->record->update_meta( 'queue', '1' ); return $import_queue; } /** * Magic method override. * * @since 4.6.16 * * @param string $key * * @return Tribe__Events__Aggregator__Record__Activity */ public function __get( $key ) { switch ( $key ) { case 'activity': return $this->activity(); break; } } /** * Returns the queue activity. * * In this implementation really stored on the record. * * @since 4.6.16 * * @return Tribe__Events__Aggregator__Record__Activity */ public function activity() { return $this->record->activity(); } /** * Shortcut to check if this queue is empty. * * @since 4.6.16 * * @return boolean `true` if this queue instance has acquired the lock and * the count is 0, `false` otherwise. */ public function is_empty() { return $this->count() === 0; } /** * Shortcut to check how many items are going to be processed next * * @since 4.6.16 * * @return int */ public function count() { $queue_status = $this->get_queue_process_status(); $total = (int) Tribe__Utils__Array::get( $queue_status, 'total', 0 ); $done = (int) Tribe__Utils__Array::get( $queue_status, 'done', 0 ); return max( 0, $total - $done ); } /** * Returns the process status of the queue, read from the queue meta. * * @since 4.6.16 * * @return array */ protected function get_queue_process_status() { $queue_status = []; if ( ! empty( $this->record->meta['queue_id'] ) ) { $queue_id = $this->record->meta['queue_id']; $queue_status = Tribe__Process__Queue::get_status_of( $queue_id )->to_array(); } return $queue_status; } /** * Processes a batch for the queue * * The `batch_size` is ignored in async mode. * * @since 4.6.16 * * @return Tribe__Events__Aggregator__Record__Async_Queue */ public function process( $batch_size = null ) { $initialized = $this->maybe_init_queue(); if ( $initialized && ! $this->is_in_progress() ) { $this->record->update_meta( 'in_progress', true ); $this->queue_process->dispatch(); } return $this; } /** * Initializes the async queue process if required. * * @since 4.6.16 * * @return bool Whether the queue needed and was correctly initialized or not. */ protected function maybe_init_queue() { if ( null === $this->queue_process ) { $queue_id = Tribe__Utils__Array::get( $this->record->meta, 'queue_id', false ); if ( false === $queue_id ) { /** * If there are no items to process then no queue process will have * been built. * But in this case it's fine: we're done and the process should be marked * as successfully completed. */ $this->record->delete_meta( 'queue' ); $this->record->delete_meta( 'in_progress' ); $this->record->set_status_as_success(); return false; } $this->queue_process = new Tribe__Events__Aggregator__Processes__Import_Events(); $this->queue_process->set_id( $queue_id ); $this->queue_process->set_record_id( $this->record->id ); return true; } return true; } /** * Indicates if the queue for the current event is actively being processed. * * @since 4.6.16 * * @return bool */ public function is_in_progress() { return isset( $this->record->meta['in_progress'] ); } /** * Sets a flag to indicate that update work is in progress for a specific event. * * No-op as the async queue has its own lock system. * * @since 4.6.16 */ public function set_in_progress_flag() { // no-op } /** * Clears the in progress flag. * * No-op as the async queue has its own lock system. * * @since 4.6.16 */ public function clear_in_progress_flag() { // no-op } /** * Returns the total progress made on processing the queue so far as a percentage. * * @since 4.6.16 * * @return int */ public function progress_percentage() { $queue_status = $this->get_queue_process_status(); $total = (int) Tribe__Utils__Array::get( $queue_status, 'total', 0 ); $done = (int) $this->record->activity()->count( Tribe__Events__Main::POSTTYPE ); if ( 0 === $total ) { return 100; } return min( 100, max( 1, (int) ( 100 * ( $done / $total ) ) ) ); } /** * Returns the primary post type the queue is processing * * @since 4.6.16 * * @return string */ public function get_queue_type() { $item_type = Tribe__Events__Main::POSTTYPE; if ( ! empty( $this->record->origin ) && 'csv' === $this->record->origin ) { $item_type = $this->record->meta['content_type']; } return $item_type; } /** * Generates a transitional id that will be used to uniquely identify dependencies in the * context of an import. * * @since 4.6.16 * * @return string An 8 char long unique ID. */ protected function generate_transitional_id() { return substr( md5( uniqid( '', true ) ), 0, 8 ); } /** * Whether the current queue process is stuck or not. * * @since 4.6.21 * * @return bool */ public function is_stuck() { if ( ! empty( $this->record->meta['queue_id'] ) ) { $queue_id = $this->record->meta['queue_id']; return Tribe__Process__Queue::is_stuck( $queue_id ); } return false; } /** * Orderly closes the queue process. * * @since 4.6.21 * * @return bool */ public function kill_queue() { if ( ! $this->record ) { return false; } if ( ! empty( $this->record->meta['queue_id'] ) ) { Tribe__Process__Queue::delete_queue( $this->record->meta['queue_id'] ); } $this->error = __( 'Unable to process this import - a breakage or conflict may have resulted in the import halting.', 'the-events-calendar' ); $this->record->delete_meta( 'in_progress' ); $this->record->delete_meta( 'queue' ); $this->record->delete_meta( 'queue_id' ); $this->record->set_status_as_failed( new WP_Error( 'stuck-queue', $this->error ) ); return true; } /** * Whether the current queue process failed or not. * * @since 4.6.21 * * @return bool */ public function has_errors() { return ! empty( $this->error ); } /** * This Queue never fetches on external resources so is always `false`. * * @return bool The state of the queue with external resources. */ public function is_fetching() { return false; } /** * Returns the queue error message. * * @since 4.6.21 * * @return string */ public function get_error_message() { return $this->error; } } PKE [Შ]ICS.phpnu[ empty( $this->meta['file'] ) ? null : $this->meta['file'], ]; $meta = wp_parse_args( $meta, $defaults ); return parent::create( $type, $args, $meta ); } /** * Public facing Label for this Origin * * @return string */ public function get_label() { return __( 'ICS', 'the-events-calendar' ); } /** * Filters the event to ensure that fields are preserved that are not otherwise supported by ICS * * @param array $event Event data * @param Tribe__Events__Aggregator__Record__Abstract $record Aggregator Import Record * * @return array */ public static function filter_event_to_preserve_fields( $event, $record ) { if ( 'ics' !== $record->origin ) { return $event; } return self::preserve_event_option_fields( $event ); } } PKE [0W  Unsupported.phpnu[delete(); } } PKE [桵iCal.phpnu[origin ) { return $event; } return self::preserve_event_option_fields( $event ); } } PKE [tM Activity.phpnu[ [], 'updated' => [], 'skipped' => [], 'scheduled' => [], ]; public function __construct() { // The items are registered on the wakeup to avoid saving unnecessary data $this->__wakeup(); } /** * Register the Activities Tracked */ public function __wakeup() { // Entry for Events CPT $this->register( Tribe__Events__Main::POSTTYPE, array( 'event', 'events' ) ); // Entry for Organizers CPT $this->register( Tribe__Events__Organizer::POSTTYPE, array( 'organizer', 'organizers' ) ); // Entry for Venues CPT $this->register( Tribe__Events__Venue::POSTTYPE, array( 'venue', 'venues' ) ); // Entry for Terms in Events Cat $this->register( Tribe__Events__Main::TAXONOMY, array( 'category', 'categories', 'cat', 'cats' ) ); // Entry for Tags $this->register( 'post_tag', array( 'tag', 'tags' ) ); // Entry for Attachment $this->register( 'attachment', array( 'attachments', 'image', 'images' ) ); /** * Fires during record activity wakeup to allow other plugins to inject/register activity entries * for other custom post types * * @param Tribe__Events__Aggregator__Record__Activity $this */ do_action( 'tribe_aggregator_record_activity_wakeup', $this ); } /** * Prevents Mapping to be saved on the DB object * @return array */ public function __sleep() { return array( 'items', 'last_status' ); } /** * Register a Specific Activity and it's mappings * * @param string $slug Name of this Activity * @param array $map (optional) Other names in which you can access this activity * * @return boolean [description] */ public function register( $slug, $map = array() ) { if ( empty( $this->items[ $slug ] ) ) { // Clone the Default action values $this->items[ $slug ] = (object) self::$actions; } else { $this->items[ $slug ] = (object) array_merge( (array) self::$actions, (array) $this->items[ $slug ] ); } // Add the base mapping $this->map[ $slug ] = $slug; // Allow short names for the activities foreach ( $map as $to ) { $this->map[ $to ] = $slug; } $this->prevent_duplicates_between_item_actions( $slug ); return true; } /** * Logs an Activity * * @param string $slug Name of this Activity * @param string|array $items Type of activity * @param array $ids items inside of the action * * @return boolean */ public function add( $slug, $items, $ids = array() ) { if ( ! $this->exists( $slug ) ) { return false; } if ( ! isset( $this->map[ $slug ] ) ) { return false; } // Map the Slug $slug = $this->map[ $slug ]; if ( is_scalar( $items ) ) { // If it's a scalar and it's not one of the registered actions we skip it if ( ! isset( self::$actions[ $items ] ) ) { return false; } // Make the actual Array of items $items = array( $items => $ids ); } else { $items = (object) $items; // Doesn't contain any of the Possible Actions if ( 0 === count( array_intersect_key( self::$actions, (array) $items ) ) ) { return false; } } foreach ( $items as $action => $ids ) { // Skip Empty ids if ( empty( $ids ) ) { continue; } $this->items[ $slug ]->{ $action } = array_unique( array_filter( array_merge( $this->items[ $slug ]->{ $action }, (array) $ids ) ) ); } return true; } /** * Returns the merged version of two Activities classes * * @param self $activity Which activity should be merged here * * @return self */ public function merge( self $activity ) { $items = $activity->get(); foreach ( $items as $slug => $data ) { $this->add( $slug, $data ); } return $this; } /** * Removes a activity from the Registered ones * * @param string $slug The Slug of the Activity * * @return boolean */ public function remove( $slug ) { if ( ! $this->exists( $slug ) ) { return false; } if ( ! isset( $this->map[ $slug ] ) ) { return false; } // Map the Slug $slug = $this->map[ $slug ]; // Remove it unset( $this->items[ $slug ] ); return true; } /** * Fetches a registered Activity * * @param string $slug (optional) The Slug of the Activity * @param string $action (optional) Which action * * @return null|array|object */ public function get( $slug = null, $action = null ) { if ( is_null( $slug ) ) { return $this->items; } if ( ! isset( $this->map[ $slug ] ) ) { return null; } // Map the Slug $slug = $this->map[ $slug ]; // Check if it actually exists if ( empty( $this->items[ $slug ] ) ) { return null; } $actions = $this->items[ $slug ]; // If we trying to get a specific action and if ( is_null( $action ) ) { return $this->items[ $slug ]; } elseif ( ! empty( $actions->{ $action } ) ) { return $actions->{ $action }; } else { return null; } } /** * Fetches a registered Activity counter * * @param string $slug (optional) The Slug of the Activity * @param string $action (optional) Which action * * @return int */ public function count( $slug = null, $action = null ) { $actions = $this->get( $slug ); if ( empty( $actions ) ) { return 0; } // Sum all of the Actions if ( is_null( $action ) ) { // recursively convert to associative array $actions = json_decode( json_encode( $actions ), true ); return array_sum( array_map( 'count', $actions ) ); } elseif ( ! empty( $actions->{ $action } ) ) { return count( $actions->{ $action } ); } return 0; } /** * Checks if a given Activity type exists * * @param string $slug The Slug of the Tab * * @return boolean */ public function exists( $slug ) { if ( is_null( $slug ) ) { return false; } if ( ! isset( $this->map[ $slug ] ) ) { return false; } // Map the Slug $slug = $this->map[ $slug ]; // Check if it actually exists return ! empty( $this->items[ $slug ] ) ; } /** * Checks the activities for a slug to make sure there are no incoherent duplicate entries due to concurring processes. * * @since 4.5.12 * * @param string $slug */ protected function prevent_duplicates_between_item_actions( $slug ) { // sanity check the updated elements: elements cannot be created AND updated if ( ! empty( $this->items[ $slug ]->updated ) && ! empty( $this->items[ $slug ]->created ) ) { $this->items[ $slug ]->updated = array_diff( $this->items[ $slug ]->updated, $this->items[ $slug ]->created ); } } /** * Returns the raw items from the activity. * * @since 4.6.15 * * @return array */ public function get_items() { return $this->items; } /** * Sets the last status on the activity object. * * Ideally set to one of the `STATUS_` constants defined by the class * but allowing arbitrary stati by design. It's up to the client to set * and consume this information. * * @since 4.6.15 * * @param string $status */ public function set_last_status( $status ) { $this->last_status = $status; } /** * Gets the last status on the activity object. * * Ideally set to one of the `STATUS_` constants defined by the class * but allowing arbitrary stati by design. It's up to the client to set * and consume this information. * * @since 4.6.15 * * @return string */ public function get_last_status() { return $this->last_status; } } PKE [C3: : Items.phpnu[items = $items; $this->original_items = $items; } /** * Returns the items as modified by the class. * * @since 4.6.16 * * @return array */ public function get_items() { return $this->items; } /** * Resets, or sets, the items the class should handle. * * @since 4.6.16 * * @param array $items */ public function set_items( array $items ) { $this->items = $items; $this->original_items = $items; } /** * Parses the items to find those that depend on linked posts defined by other items * and marks them as dependent. * * @since 4.6.16 * * @return $this */ public function mark_dependencies() { $items = $this->items; $venue_global_ids = []; $organizer_global_ids = []; foreach ( $items as &$item ) { $item = (object) $item; $item->depends_on = []; if ( isset( $item->venue ) ) { $venue = (object) $item->venue; if ( ! isset( $venue->global_id ) ) { continue; } $venue_global_id = $venue->global_id; if ( in_array( $venue_global_id, $venue_global_ids, true ) ) { $item->depends_on[] = $venue_global_id; } else { $venue_global_ids[] = $venue_global_id; } } if ( isset( $item->organizer ) ) { $organizers = $item->organizer; if ( is_object( $item->organizer ) ) { $organizers = [ $item->organizer ]; } foreach ( $organizers as $organizer ) { $organizer = (object) $organizer; if ( ! isset( $organizer->global_id ) ) { continue; } $organize_global_id = $organizer->global_id; if ( in_array( $organize_global_id, $organizer_global_ids, true ) ) { $item->depends_on[] = $organize_global_id; } else { $organizer_global_ids[] = $organize_global_id; } } } if ( empty( $item->depends_on ) ) { unset( $item->depends_on ); } } $this->items = $items; return $this; } /** * Returns the items originally set via the constructor the `set_items` method. * * @since 4.6.16 * * @return array */ public function get_original_items() { return $this->original_items; } } PKE AUrl.phpnu[meta['import_id'] ) ) { return []; } $import_id = $record->meta['import_id']; /** @var \wpdb $wpdb */ global $wpdb; $pending_status = Records::$status->pending; $query = $wpdb->prepare( "SELECT ID FROM {$wpdb->postmeta} pm JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_type = %s AND p.post_status = %s AND pm.meta_key = '_tribe_aggregator_import_id' AND pm.meta_value = %s ORDER BY p.post_modified_gmt DESC", Records::$post_type, $pending_status, $import_id ); /** * Filters the query to find duplicate pending import records in respect to an * import id. * * If the filter returns an empty value then the delete operation will be voided. * This is a maintenance query that should really run so take care while modifying * or voiding it! * * @param string $query The SQL query used to find duplicate pending import records * in respect to an import id. */ $query = apply_filters( 'tribe_aggregator_import_queue_cleaner_query', $query ); if ( empty( $query ) ) { return []; } $records = $wpdb->get_col( $query ); array_shift( $records ); $deleted = []; foreach ( $records as $to_delete ) { $post = wp_delete_post( $to_delete, true ); if ( ! empty( $post ) ) { $deleted[] = $post->ID; } } return $deleted; } /** * Depending from how long a record has been pending and the allowed lifespan * update the record status to failed. * * @param Tribe__Events__Aggregator__Record__Abstract $record * * @return bool If the record status has been set to failed or not. */ public function maybe_fail_stalled_record( Tribe__Events__Aggregator__Record__Abstract $record ) { $pending = Records::$status->pending; $failed = Records::$status->failed; $post_status = $record->post->post_status; if ( ! in_array( $post_status, [ $pending, $failed ], true ) ) { return false; } $id = $record->post->ID; if ( $post_status === $failed ) { tribe( 'logger' )->log_debug( "Record {$record->id} is failed: deleting its queue information", 'Queue_Cleaner' ); delete_post_meta( $id, '_tribe_aggregator_queue' ); Tribe__Post_Transient::instance()->delete( $id, '_tribe_aggregator_queue' ); return true; } $created = strtotime( $record->post->post_date ); $last_updated = strtotime( $record->post->post_modified_gmt ); $now = time(); $since_creation = $now - $created; $pending_for = $now - $last_updated; if ( $pending_for > $this->get_stall_limit() || $since_creation > $this->get_time_to_live() ) { tribe( 'logger' )->log_debug( "Record {$record->id} has stalled for too long: deleting it and its queue information", 'Queue_Cleaner' ); $failed = Tribe__Events__Aggregator__Records::$status->failed; wp_update_post( [ 'ID' => $id, 'post_status' => $failed ] ); delete_post_meta( $id, '_tribe_aggregator_queue' ); Tribe__Post_Transient::instance()->delete( $id, '_tribe_aggregator_queue' ); return true; } return false; } /** * Allow external caller to define the amount of the time to live in seconds. * * @since 5.3.0 * * @param int $time_to_live Live time in seconds default to 12 hours. * * @return $this */ public function set_time_to_live( $time_to_live ) { $this->time_to_live = (int) $time_to_live; return $this; } /** * Get the current value of time to live setting an integer in seconds, default to 12 hours. * * @since 5.3.0 * * @return int The number of time to consider a record alive. */ public function get_time_to_live() { /** * Allow to define the number of seconds used to define if a record is alive or not. * * @since 5.3.0 * * @return int The number of time to consider a record alive. */ return (int) apply_filters( 'tribe_aggregator_import_queue_cleaner_time_to_live', $this->time_to_live ); } /** * Gets the time, in seconds, after which a pending record is considered stalling. * * @return int The number in seconds for a record to be stalled */ public function get_stall_limit() { /** * Allow to define the number of seconds for a record to be considered stalled. * * @since 5.3.0 * * @return int The number in seconds for a record to be stalled */ return (int) apply_filters( 'tribe_aggregator_import_queue_cleaner_stall_limit', $this->stall_limit ); } /** * Sets the time, in seconds, after which a pending record is considered stalling. * * @param int $stall_limit Allow to set the stall limit of a record. */ public function set_stall_limit( $stall_limit ) { $this->stall_limit = $stall_limit; return $this; } } PKE [|,''Queue_Realtime.phpnu[queue = $queue; $this->ajax_operations = $ajax_operations ? $ajax_operations : new Tribe__Events__Ajax__Operations; $this->queue_processor = $queue_processor ? $queue_processor : tribe( 'events-aggregator.main' )->queue_processor; } /** * Adds additional data to the tribe_aggregator object (available to our JS). */ public function update_loop_vars() { $percentage = $this->queue->progress_percentage(); $progress = $this->sanitize_progress( $percentage ); $data = [ 'record_id' => $this->record_id, 'check' => $this->get_ajax_nonce(), 'completeMsg' => __( 'Completed!', 'the-events-calendar' ), 'progress' => $progress, 'progressText' => sprintf( __( '%d%% complete', 'the-events-calendar' ), $progress ), ]; wp_localize_script( 'tribe-ea-notice', 'tribe_aggregator_save', $data ); return $data; } public function render_update_message() { if ( ! Tribe__Events__Aggregator__Page::instance()->aggregator_should_load_scripts() ) { return false; } /** @var Tribe__Events__Aggregator__Record__Queue_Processor $processor */ $processor = tribe( 'events-aggregator.main' )->queue_processor; if ( ! $this->record_id = $processor->next_waiting_record( true ) ) { return false; } $this->queue = $this->queue ? $this->queue : Tribe__Events__Aggregator__Record__Queue_Processor::build_queue( $this->record_id ); if ( $this->queue->is_empty() ) { return false; } $this->update_loop_vars(); ob_start(); $percent = $this->sanitize_progress( $this->queue->progress_percentage() ); ?>

render( 'aggregator-update-msg', $html ); } /** * Action to reply every time a heart beat is executed to send the progress of EA if an EA record is present. * * @since 5.4.0 Change the method signature to be a little bit less aggressive with enforcing types. * * @param array $response The current response object. * @param array $data An array with the data from the client. * * @return array An array used to construct the heart beat response. */ public function receive_heartbeat( $response, $data ) { if ( empty( $data['ea_record'] ) ) { return $response; } $this->record_id = absint( $data['ea_record'] ); if ( 0 === $this->record_id ) { return $response; } $data = $this->get_queue_progress_data(); if ( empty( $data ) ) { return $response; } $response['ea_progress'] = $data; return $response; } /** * Handle queue ajax requests */ public function ajax() { $this->record_id = (int) tribe_get_request_var( 'record' ); // Nonce check. $this->ajax_operations->verify_or_exit( tribe_get_request_var( 'check' ), $this->get_ajax_nonce_action(), $this->get_unable_to_continue_processing_data() ); $data = $this->get_queue_progress_data(); $exit_data = empty( $data ) ? '' : wp_json_encode( $data ); $this->ajax_operations->exit_data( $exit_data ); } /** * @param $percentage * * @return int|string */ private function sanitize_progress( $percentage ) { if ( $percentage === true ) { return 100; } return is_numeric( $percentage ) ? intval( $percentage ) : 0; } /** * @return string */ public function get_ajax_nonce() { return wp_create_nonce( $this->get_ajax_nonce_action() ); } /** * Generates the nonce action string on an event and user base. * * @param int|null $event_id An event post ID to override the instance defined one. * * @return string */ public function get_ajax_nonce_action( $record_id = null ) { $record_id = $record_id ? $record_id : $this->record_id; return 'tribe_aggregator_insert_items_' . $record_id . get_current_user_id(); } /** * @return mixed|string|void */ public function get_unable_to_continue_processing_data() { return json_encode( [ 'html' => __( 'Unable to continue inserting data. Please reload this page to continue/try again.', 'the-events-calendar' ), 'progress' => false, 'continue' => false, 'complete' => false, ] ); } /** * Returns the progress message data. * * @param Tribe__Events__Aggregator__Record__Queue_Interface $queue * @param int $percentage * @param bool $done * * @return mixed|string|void */ public function get_progress_message_data( $queue, $percentage, $done ) { return wp_json_encode( $this->get_progress_data( $queue, $percentage, $done ) ); } /** * Get the data that is used to construct the current status of the EA progress bar. * * @since 5.3.0 * * @return array An array with the details of the progress bar. */ private function get_queue_progress_data() { if ( (int) $this->record_id <= 0 ) { return []; } // Load the queue. /** @var \Tribe__Events__Aggregator__Record__Queue_Interface $queue */ $queue = $this->queue ? $this->queue : Tribe__Events__Aggregator__Record__Queue_Processor::build_queue( $this->record_id ); // We always need to set up the Current Queue. $this->queue_processor->set_current_queue( $queue ); // Only if it's not empty that we care about processing. if ( ! $queue->is_empty() ) { $this->queue_processor->process_batch( $this->record_id ); } /** * Include current queue to prevent progress bar from sticking on csv imports * * @var \Tribe__Events__Aggregator__Record__Queue_Interface $current_queue */ $current_queue = $this->queue_processor->current_queue; $done = $current_queue->is_empty() && empty( $current_queue->is_fetching() ); $percentage = $current_queue->progress_percentage(); return $this->get_progress_data( $current_queue, $percentage, $done ); } /** * Get the current Queue status for EA to consume the status of the progress bar. * * @since 5.3.0 * * @param Tribe__Events__Aggregator__Record__Queue_Interface $queue The Queue being processed. * @param int $percentage The amount of the percentage. * @param bool $done If the Import was completed or not. * * @return array Get an array with the details of the current Queue. */ private function get_progress_data( $queue, $percentage, $done ) { $queue_type = $queue->get_queue_type(); $is_event_queue = $queue_type === Tribe__Events__Main::POSTTYPE; $activity = $queue->activity(); $error = $queue->has_errors(); $data = [ 'html' => false, 'progress' => $percentage, 'progress_text' => sprintf( __( '%d%% complete', 'the-events-calendar' ), $percentage ), 'continue' => ! $done, 'complete' => $done, 'error' => $error, 'counts' => [ 'total' => $activity->count( $queue_type ), 'created' => $activity->count( $queue_type, 'created' ), 'updated' => $activity->count( $queue_type, 'updated' ), 'skipped' => $activity->count( $queue_type, 'skipped' ), 'category' => $activity->count( 'category', 'created' ), 'images' => $activity->count( 'images', 'created' ), 'venues' => $is_event_queue ? $activity->count( 'venues', 'created' ) : 0, 'organizers' => $is_event_queue ? $activity->count( 'organizer', 'created' ) : 0, 'remaining' => $queue->count(), ], ]; $messages = Tribe__Events__Aggregator__Tabs__New::instance()->get_result_messages( $queue ); if ( $error ) { $data['error_text'] = '

' . implode( ' ', $messages['error'] ) . '

'; } elseif ( $done ) { $data['complete_text'] = '

' . implode( ' ', $messages['success'] ) . '

'; } return $data; } } PKE ['.?11Queue_Processor.phpnu[PKE [$UU `1Abstract.phpnu[PKE  x Meetup.phpnu[PKE mmEList_Table.phpnu[PKE ¶))Batch_Queue.phpnu[PKE [2-- *Queue.phpnu[PKE \YEventbrite.phpnu[PKE [$$kgCal.phpnu[PKE  NoVoid_Queue.phpnu[PKE [M44b{CSV.phpnu[PKE ה]Queue_Interface.phpnu[PKE [5-$$4Async_Queue.phpnu[PKE [Შ],ICS.phpnu[PKE [0W  ZUnsupported.phpnu[PKE [桵iCal.phpnu[PKE [tM Activity.phpnu[PKE [C3: : &Items.phpnu[PKE AUrl.phpnu[PKE [TTQueue_Cleaner.phpnu[PKE [|,''4Queue_Realtime.phpnu[PK\