File: //var/www/lipovac/wp-content/plugins/the-events-calendar/src/Tribe/Aggregator/CLI/Command.php
<?php
class Tribe__Events__Aggregator__CLI__Command {
	/**
	 * @var int The polling interval timeout in seconds.
	 */
	protected $polling_timeout = 30;
	/**
	 * @var int The polling interval in seconds.
	 */
	protected $polling_interval = 2;
	/**
	 * Run an import of the specified type from a specified source.
	 *
	 * The command will use the API and licenses set for the site if required.
	 *
	 * <origin>
	 * : the import origin type
	 * ---
	 * options:
	 *   - ical
	 *   - gcal
	 *   - csv
	 *   - ics
	 *   - facebook
	 *   - meetup
	 *   - url
	 * ---
	 *
	 * <source>
	 * : The source to import events from; a URL or a file path for .ics and CSV files.
	 *
	 * [--keywords=<keywords>]
	 * : Optionally filter events by these keywords.
	 *
	 * [--location=<location>]
	 * : Filter events by this location, not supported by all origin types.
	 *
	 * [--radius=<radius>]
	 * : Only fetch events in this mile radius around the location.
	 * Will be ignored if the `--location` parameter is not set.
	 *
	 * [--start=<start>]
	 * : Only fetch events starting after this date.
	 * This should be a valid date string or a value supported by the `strtotime` PHP function.
	 * Not supported by all origin types.
	 *
	 * [--end=<end>]
	 * : Only fetch events starting before this date.
	 * This should be a valid date string or a value supported by the `strtotime` PHP function.
	 * When using natural language expressions keep in mind that those apply from the current time, not start.
	 * Not supported by all origin types.
	 * Defaults the range set in the import settings for this origin type.
	 *
	 * [--limit_type=<limit_type>]
	 * : The type of limit that should be used to limit the number of fetched events.
	 * ---
	 * options:
	 *   - count
	 *   - range
	 *   - no_limit
	 * ---
	 *
	 * [--limit=<limit>]
	 * : The value of the limit that should be applied; ignored if `--limit_type` is not set or set to `no_limit`.
	 * Either a value in seconds if the `--limit_type` is range or a number if `--limit_type` is set to `count`.
	 * When importing CSV files this limit will NOT apply.
	 *
	 * [--timeout=<timeout>]
	 * : How long should the command wait for the data from EA Service in seconds
	 * ---
	 * default: 30
	 * ---
	 *
	 * [--post_status=<post_status>]
	 * : The post status that should be assigned to the imported events; default to the one set in Import options.
	 * ---
	 * options:
	 *   - publish
	 *   - draft
	 *   - pending
	 *   - private
	 * ---
	 *
	 * [--category=<category>]
	 * : An optional category that should be assigned to the imported events.
	 *
	 * [--content_type=<content_type>]
	 * : The type of import for CSV files.
	 * The column mapping must be defined with the `--column_map` parameter.
	 * ---
	 * default: events
	 * options:
	 *   - events
	 *   - venues
	 *   - organizers
	 * ---
	 *
	 * [--column_map=<column_map>]
	 * : the column mapping that should be used for CSV imports; required when runnin CSV imports. A comma separated
	 * list where the order counts.
	 * For events the available columns are: name, description, excerpt, start_date, start_time, end_date, end_time,
	 * timezone, all_day, hide, sticky, venue_name, organizer_name, show_map_link, show_map, cost, currency_symbol,
	 * currency_position, category, tags, website, comment_status, ping_status, featured_image, feature_event
	 * For venues the available columns are: name, description, country, address, address2, city, state, zip, phone,
	 * url, featured_image
	 * For organizers the available columns are: name, description, email, website, phone, featured_image
	 *
	 * [--format=<format>]
	 * : The results output format
	 * ---
	 *
	 * ## Examples
	 *
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --start=tomorrow --end="+3 weeks"
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --limit_type=count --limit=20
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --location="Toronto" --radius=50
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --keywords=Party
	 *      wp event-aggregator import-from meetup https://www.meetup.com/wordpress-ile-de-france/
	 *      wp event-aggregator import-from gcal https://calendar.google.com/calendar/ical/me/public/basic.ics
	 *      wp event-aggregator import-from csv /Users/moi/events.csv --content_type=events --column_map=name,description,start_date,start_time,end_date,end_time
	 *      wp event-aggregator import-from ics /Users/moi/events.ics
	 *
	 *
	 * @since      4.6.15
	 *
	 * @subcommand import-from
	 *
	 * @when       after_wp_load
	 */
	public function import_from_source( array $args, array $assoc_args = array() ) {
		$this->ensure_timeout( $assoc_args );
		list( $origin, $source ) = $args;
		$is_csv = 'csv' === $origin;
		if ( $is_csv ) {
			$this->ensure_column_map( $assoc_args );
		}
		$record = $this->create_record_from( $assoc_args, $origin, $source );
		$this->fetch_and_process( $assoc_args, $record, $is_csv );
	}
	/**
	 * Check the timeout parameter if set.
	 *
	 * @since 4.6.15
	 */
	protected function ensure_timeout( array $assoc_args ) {
		if ( isset( $assoc_args['timeout'] ) && ! is_numeric( $assoc_args['timeout'] ) ) {
			WP_CLI::error( 'The timeout should be a numeric value.' );
		}
	}
	/**
	 * Creates a new record.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 * @param string $origin
	 * @param string $source
	 *
	 * @return Tribe__Events__Aggregator__Record__Abstract
	 */
	protected function create_record_from( array $assoc_args, $origin, $source ) {
		$is_csv = 'csv' === $origin;
		$types = array(
			'ical' => 'Tribe__Events__Aggregator__Record__iCal',
			'gcal' => 'Tribe__Events__Aggregator__Record__gCal',
			'csv' => 'Tribe__Events__Aggregator__Record__CSV',
			'ics' => 'Tribe__Events__Aggregator__Record__ICS',
			'meetup' => 'Tribe__Events__Aggregator__Record__Meetup',
			'url' => 'Tribe__Events__Aggregator__Record__Url',
		);
		$record_class = Tribe__Utils__Array::get( $types, $origin, reset( $types ) );
		/** @var Tribe__Events__Aggregator__Record__Abstract $record */
		$record = new $record_class;
		$record_args = array();
		$location   = Tribe__Utils__Array::get( $assoc_args, 'location' );
		$limit_type = Tribe__Utils__Array::get( $assoc_args, 'limit_type' );
		$limit      = Tribe__Utils__Array::get( $assoc_args, 'limit' );
		if ( isset( $assoc_args['start'], $assoc_args['end'] ) ) {
			$limit_type = 'no_limit';
			$limit      = 'not_set';
		}
		$category    = Tribe__Utils__Array::get( $assoc_args, 'category', '' );
		$category_id = '';
		if ( is_numeric( $category ) ) {
			$category_id = $category;
		} elseif ( ! empty( $category ) ) {
			$term = get_term_by( 'slug', $category, Tribe__Events__Main::TAXONOMY );
			if ( ! $term instanceof WP_Term ) {
				WP_CLI::error( "$category is not a valid category ID or slug." );
			}
			$category_id = $term->term_id;
		}
		$record_meta = array(
			'origin' => $origin,
			'type' => 'manual',
			'keywords' => Tribe__Utils__Array::get( $assoc_args, 'keywords', '' ),
			'location' => $location,
			'start' => Tribe__Utils__Array::get( $assoc_args, 'start' ),
			'end' => Tribe__Utils__Array::get( $assoc_args, 'end' ),
			'radius' => $location ? Tribe__Utils__Array::get( $assoc_args, 'radius' ) : null,
			'limit_type' => $limit_type,
			'limit' => $limit,
			'source' => $source,
			'preview' => false,
			'category' => $category_id,
		);
		if ( ! empty( $record_meta['start'] ) ) {
			$record_meta['start'] = Tribe__Date_Utils::reformat( $record_meta['start'], 'Y-m-d H:i:s' );
			if ( empty( $record_meta['start'] ) ) {
				WP_CLI::error( 'The --start parameter could not be parsed; review the argument description.' );
			}
		}
		if ( ! empty( $record_meta['end'] ) ) {
			$record_meta['end'] = Tribe__Date_Utils::reformat( $record_meta['end'], 'Y-m-d H:i:s' );
			if ( empty( $record_meta['end'] ) ) {
				WP_CLI::error( 'The --end parameter could not be parsed; review the argument description.' );
			}
		}
		if ( isset( $record_meta['start'], $record_meta['end'] ) ) {
			if ( strtotime( $record_meta['end'] ) < strtotime( $record_meta['start'] ) ) {
				WP_CLI::error( "End date [{$record_meta['end']}] cannot be before start date [{$record_meta['start']}]; review the argument description." );
			}
		}
		if ( $is_csv ) {
			$record_meta['file']         = $source;
			$record_meta['content_type'] = Tribe__Utils__Array::get( $assoc_args, 'content_type', 'events' );
		}
		if ( 'ics' === $origin ) {
			$record_meta['file'] = array(
				'name' => $source,
				'tmp_name' => $source,
			);
		}
		if ( empty( $assoc_args['post_status'] ) ) {
			$record_meta['post_status'] = tribe( 'events-aggregator.settings' )->default_post_status( $origin );
		} else {
			$record_meta['post_status'] = $assoc_args['post_status'];
		}
		WP_CLI::log( 'Creating record post...' );
		$created = $record->create( 'manual', $record_args, $record_meta );
		if ( $created instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while creating the import: ' . $created->get_error_message() );
		}
		WP_CLI::log( "Record created with post ID {$record->id}." );
		return $record;
	}
	/**
	 * Fetches the data from the Service and processes it.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 * @param Tribe__Events__Aggregator__Record__Abstract $record
	 * @param bool $is_csv
	 *
	 * @return array
	 */
	protected function fetch_and_process( array $assoc_args, $record, $is_csv ) {
		$queue_import_args         = array();
		// remove anything that cannot be serialized
		foreach ( $record->meta as $key => $value ) {
			if ( is_array( $value ) || is_scalar( $value ) ) {
				$queue_import_args[ $key ] = $value;
			}
		}
		$queue_result = $record->queue_import( $queue_import_args );
		$record->finalize();
		$this->polling_timeout = (float) $assoc_args['timeout'];
		if ( $is_csv ) {
			$column_map = $assoc_args['column_map'];
			$activity   = $this->import_csv_file( $queue_result, $record, $column_map );
		} else {
			$activity = $this->import_from_service( $queue_result, $record );
		}
		if ( ! $activity instanceof Tribe__Events__Aggregator__Record__Activity ) {
			if ( $activity instanceof WP_Error ) {
				WP_CLI::error( $activity->get_error_message() );
			} else {
				WP_CLI::error( 'Something went wrong during the import process.' );
			}
			$record->set_status_as_failed();
		}
		$assoc_args['format'] = ! empty( $assoc_args['format'] ) ? $assoc_args['format'] : 'yaml';
		$items = $activity->get();
		// just a "cosmetic" refinement to make sure integers will be rendered as integers
		foreach ( $items as $type ) {
			foreach ( $type as &$action ) {
				$action = array_map( function ( $entry ) {
					return is_numeric( $entry ) ? (int) $entry : $entry;
				}, $action );
			}
		}
		WP_CLI::print_value( $items, $assoc_args );
		WP_CLI::success( 'Import done!' );
		$record->update_meta( 'activity', $activity );
		$record->delete_meta( 'queue' );
		$record->set_status_as_success();
		return $action;
	}
	/**
	 * Imports a CSV file.
	 *
	 * The logic to handle and import CSV files is different, primarily in it not relying on the Service, from
	 * other imports. Mind that CSV source files should have their columns in exactly the same order and named
	 * exactly as those found in the UI.
	 *
	 * @since 4.6.15
	 *
	 * @param Tribe__Events__Aggregator__Record__CSV $record
	 * @param array $record_meta
	 * @param string|array $column_map The column map that should be used for the import, either a comma-separated list
	 *                                 or an array.
	 *
	 * @return Tribe__Events__Aggregator__Record__Activity
	 */
	protected function import_csv_file( $queue_result, $record, $column_map ) {
		WP_CLI::log( 'Reading the file...' );
		if ( ! is_array( $column_map ) ) {
			$column_map = preg_split( '/\\s*,\\s*/', $column_map );
		}
		if ( empty( $column_map ) ) {
			WP_CLI::error( 'The provided column map is invalid.' );
		}
		$map    = array(
			'events' => 'event',
			'venues' => 'venue',
			'organizers' => 'organizer',
		);
		$prefix = Tribe__Utils__Array::get( $map, $record->meta['content_type'], 'event' );
		$column_map = array_map( function ( $key ) use ( $prefix ) {
			return $key === 'featured_image' || $key === 'feature_event' ? $key : $prefix . '_' . $key;
		}, $column_map );
		$data = array(
			'action' => 'new',
			'import_id' => $record->id,
			'origin' => 'csv',
			'csv' =>
				array(
					'content_type' => 'tribe_' . $record->meta['content_type'],
					'file' => $record->meta['file'],
				),
			'column_map' => $column_map,
			'post_status' => $record->meta['post_status'],
			'category' => $record->meta['category'],
			'selected_rows' => 'all',
		);
		$response = $queue_result;
		if ( $response instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while reading the file: ' . $response->get_error_message() );
		}
		if ( isset( $response['data']['items'] ) && $response['data']['items'] instanceof WP_Error ) {
			WP_CLI::error( $response['data']['items']->get_error_message() );
		}
		$item_name = rtrim( $record->meta['content_type'], 's' );
		$items     = $response['data']['items'];
		if ( empty( $items ) ) {
			WP_CLI::success( 'No items found matching the query.' );
		}
		$items_count = count( $items );
		WP_CLI::log( "Read data for {$items_count} {$item_name}(s) from the file." );
		add_filter( 'tribe_aggregator_batch_size', function () use ( $items_count ) {
			return $items_count + 1;
		} );
		/** @var Tribe__Events__Aggregator__Record__Queue_Interface $result */
		$result = $record->process_posts( $data );
		if ( $result instanceof WP_Error ) {
			WP_CLI::error( $result->get_error_message() );
		}
		$activity = $result->activity();
		return $activity;
	}
	/**
	 * Imports the data for a record from the Service.
	 *
	 * This is a full end-to-end handling of the request; the method will queue the import on the Service,
	 * fetch the data from it and import the returned data (if any).
	 *
	 * @param array $assoc_args
	 * @param object|WP_Error $queue_result The result of the queue operation on the Service
	 * @param Tribe__Events__Aggregator__Record__Abstract $record
	 *
	 * @return Tribe__Events__Aggregator__Record__Activity
	 */
	protected function import_from_service( $queue_result, $record ) {
		$item_name = 'event';
		WP_CLI::log( 'Creating import on the service...' );
		if ( $queue_result instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while queueing the import on the service: ' . $queue_result->get_error_message() );
		}
		WP_CLI::log( "Import was assigned ID  {$record->meta['import_id']} from the service." );
		$start_time = microtime( true );
		$response   = null;
		if ( $record->is_polling() ) {
			WP_CLI::log( "Data will be fetched polling the service, timeout is {$this->polling_timeout} seconds." );
			$first = true;
			do {
				if ( ! $first ) {
					sleep( $this->polling_interval );
				}
				WP_CLI::log( 'Polling service to get data...' );
				$response = $record->get_import_data();
				$first    = false;
			} while (
				( $response instanceof stdClass && 'fetching' === $response->status )
				&& microtime( true ) - $start_time <= $this->polling_timeout
			);
		} else {
			WP_CLI::error( 'Batch data pushing is not supported in the CLI yet!' );
		}
		if ( null === $response ) {
			WP_CLI::error( 'Run out of time  while waiting for the data, check the source or explicitly set the `--timeout` parameter to a higher value and retry.' );
		}
		if ( $response instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while fetching the data from the service: ' . $response->get_error_message() );
		}
		if ( ! property_exists( $response->data, 'events' ) ) {
			WP_CLI::error( 'Empty event data; response was ' . wp_json_encode( $response ) );
		}
		$items = $response->data->events;
		if ( empty( $items ) ) {
			WP_CLI::success( "No {$item_name}s found matching the query." );
		}
		$events_count = count( $items );
		WP_CLI::log( "Received data for {$events_count} event(s) from the service." );
		if ( empty( $response->data ) ) {
			WP_CLI::error( 'Empty data; response was ' . wp_json_encode( $response ) );
		}
		$progress = WP_CLI\Utils\make_progress_bar( 'Inserting posts', $events_count, $interval = 100 );
		// here we use the filter as an action to tick the progress
		add_action( 'tribe_aggregator_before_save_event', function ( array $event ) use ( $progress ) {
			$progress->tick();
			return $event;
		} );
		/** @var Tribe__Events__Aggregator__Record__Activity $activity */
		$activity = $record->insert_posts( $items );
		$progress->finish();
		return $activity;
	}
	/**
	 * Run a schuduled import.
	 *
	 * The command will use the API and licenses set for the site if required.
	 *
	 * <import_id>
	 * : the import ID, i.e. the import post ID in the site database
	 *
	 * [--timeout=<timeout>]
	 * : How long should the command wait for the data from EA Service in seconds
	 * ---
	 * default: 30
	 * ---
	 *
	 * [--format=<format>]
	 * : The results output format
	 * ---
	 *
	 * ## Examples
	 *
	 *      wp event-aggregator run-import 2389
	 *      wp event-aggregator run-import 2389 --timeout=180
	 *
	 * @since      4.6.15
	 *
	 * @subcommand run-import
	 *
	 * @when       after_wp_load
	 */
	public function run_import( array $args, array $assoc_args = array() ) {
		$this->ensure_timeout( $assoc_args );
		$record_id = $args[0];
		/** @var Tribe__Events__Aggregator__Records $records */
		$records = tribe( 'events-aggregator.records' );
		$parent_record = $records->get_by_post_id( $record_id );
		if ( ! $parent_record instanceof Tribe__Events__Aggregator__Record__Abstract ) {
			WP_CLI::error( "No scheduled record with a post ID of {$record_id} was found." );
		}
		// this should not be possible, yet let's take the possibility into account
		$is_csv = 'csv' === $parent_record->meta['origin'];
		if ( $is_csv ) {
			$this->ensure_column_map( $assoc_args );
		}
		WP_CLI::log( 'Creating child import post...' );
		$record = $parent_record->create_child_record();
		if ( ! $record instanceof Tribe__Events__Aggregator__Record__Abstract ) {
			if ( $record instanceof WP_Error ) {
				WP_CLI::error( "Could not create child record for record {$record_id}: " . $record->get_error_message() );
			} else {
				WP_CLI::error( "Could not create child record for record {$record_id}." );
			}
		}
		WP_CLI::log( "Created child import post with ID {$record->id}" );
		$record->update_meta( 'interactive', true );
		$this->fetch_and_process( $assoc_args, $record, $is_csv );
	}
	/**
	 * Checks the associative arguments to make sure the column map is provided for CSV imports.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 */
	protected function ensure_column_map( array $assoc_args = array() ) {
		if ( ! isset( $assoc_args['column_map'] ) ) {
			WP_CLI::error( 'the --column_map argument is required when importing CSV files.' );
		}
		$split = preg_split( '/\\s*,\\s*/', $assoc_args['column_map'] );
		if ( empty( $split ) ) {
			WP_CLI::error( 'the --column_map argument should contain a comma-separated list of column names for CSV imports.' );
		}
	}
}