8889841cPK E['.?1 1 Queue_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 ); } } PK E[$U U 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 ) ); } } PK E[x 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; } } PK E[Am m List_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 '
' . esc_html( $title ) . '
'; } $html[] = '' . esc_html_x( 'via ', 'record via origin', 'the-events-calendar' ) . '' . $source_info['via'] . '
'; $html[] = '' . esc_html__( 'The following fields are required for a successful import:', 'the-events-calendar' ) . '
'; $message .= '' . implode( ' ', $messages['error'] ) . '
'; } elseif ( $done ) { $data['complete_text'] = '' . implode( ' ', $messages['success'] ) . '
'; } return $data; } } PK E['.?1 1 Queue_Processor.phpnu [ PK E[$U U `1 Abstract.phpnu [ PK E[x x Meetup.phpnu [ PK E[Am m E List_Table.phpnu [ PK E[E¶) ) Batch_Queue.phpnu [ PK E[2- - * Queue.phpnu [ PK E[k\ Y Eventbrite.phpnu [ PK E[$ $ k gCal.phpnu [ PK E[M No Void_Queue.phpnu [ PK E[M4 4 b{ CSV.phpnu [ PK E[iה ] Queue_Interface.phpnu [ PK E[5-$ $ 4 Async_Queue.phpnu [ PK E[Შ] , ICS.phpnu [ PK E[0W Z Unsupported.phpnu [ PK E[桵 iCal.phpnu [ PK E[tM Activity.phpnu [ PK E[C3: : & Items.phpnu [ PK E[A A Url.phpnu [ PK E[T T Queue_Cleaner.phpnu [ PK E[|,' ' 4 Queue_Realtime.phpnu [ PK \