is_valid( $cal ) ) { try { $result = $this->add_vcalendar_events_to_db( $arguments ); } catch ( Ai1ec_Parse_Exception $exception ) { throw new Ai1ec_Parse_Exception( 'Processing "' . $arguments['source'] . '" triggered error: ' . $exception->getMessage() ); } return $result; } throw new Ai1ec_Parse_Exception( 'The passed string is not a valid ics feed' ); } public function events_order_comparator( $e1, $e2 ) { return strcmp( $e1->dtstart, $e2->dtstart ); } /** * Process vcalendar instance - add events to database. * * @param vcalendar $v Calendar to retrieve data from. * @param array $args Arbitrary arguments map. * * @throws Ai1ec_Parse_Exception * * @internal param array $args Array with events and configuration * * @return int Count of events added to database. */ public function add_vcalendar_events_to_db( array $args ) { $cal = $this->_normalize_google_uids( $args['source'] ); $feed = isset( $args['feed'] ) ? $args['feed'] : null; $comment_status = isset( $args['comment_status'] ) ? $args['comment_status'] : 'open'; $do_show_map = isset( $args['do_show_map'] ) ? $args['do_show_map'] : 0; $count = 0; $events_in_db = isset( $args['events_in_db'] ) ? $args['events_in_db'] : 0; $feed_name = $feed->feed_url; // Fetch default timezone in case individual properties don't define it $local_timezone = $this->_registry->get( 'date.timezone' )->get_default_timezone(); $messages = array(); $current_timestamp = $this->_registry->get( 'date.time' )->format_to_gmt(); // initialize empty custom exclusions structure $exclusions = array(); // go over each event foreach ( $cal as $e ) { // Event data array. $data = array(); // ===================== // = Start & end times = // ===================== $start = (array)$e->dtstart; $end = (array)$e->dtend; // For cases where a "VEVENT" calendar component // specifies a "DTSTART" property with a DATE value type but none // of "DTEND" nor "DURATION" property, the event duration is taken to // be one day. For cases where a "VEVENT" calendar component // specifies a "DTSTART" property with a DATE-TIME value type but no // "DTEND" property, the event ends on the same calendar date and // time of day specified by the "DTSTART" property. if ( empty( $end ) ) { // #1 if duration is present, assign it to end time $end = $e->duration; if ( empty( $end ) ) { // #2 if only DATE value is set for start, set duration to 1 day if ( ! isset( $start['time']['hour'] ) ) { $end = array( 'date' => array( 'year' => $start['date']['year'], 'month' => $start['date']['month'], 'day' => $start['date']['day'] + 1, ), 'time' => array( 'hour' => 0, 'minute' => 0, 'second' => 0, ), ); // #3 set end date to start time $end = $start; } } } $categories = $e->categories; $imported_cat = array( Ai1ec_Event_Taxonomy::CATEGORIES => array() ); // If the user chose to preserve taxonomies during import, add categories. if( $categories && $feed->keep_tags_categories ) { $imported_cat = $this->add_categories_and_tags( $categories, $imported_cat, false, true ); } $feed_categories = $feed->feed_category; if( ! empty( $feed_categories ) ) { $imported_cat = $this->add_categories_and_tags( $feed_categories, $imported_cat, false, false ); } $tags = $e->x_tags; $imported_tags = array( Ai1ec_Event_Taxonomy::TAGS => array() ); // If the user chose to preserve taxonomies during import, add tags. if( $tags && $feed->keep_tags_categories ) { $imported_tags = $this->add_categories_and_tags( $tags, $imported_tags, true, true ); } $feed_tags = $feed->feed_tags; if( ! empty( $feed_tags ) ) { $imported_tags = $this->add_categories_and_tags( $feed_tags, $imported_tags, true, true ); } // Event is all-day if no time components are defined $allday = $this->_is_timeless( (array)$start['time'] ) && $this->_is_timeless( (array)$end['time'] ); // Also check the proprietary MS all-day field. $ms_allday = $e->x_microsoft_cdo_alldayevent; if ( ! empty( $ms_allday ) && 'TRUE' == $ms_allday ) { $allday = true; } $event_timezone = $e->timezone; // Check if the timezone is a recognized TZ in PHP // Note: the TZ may be perfectly valid, but it may not be an accepted value in the PHP version the plugin is running on try { $tz = @timezone_open( $event_timezone ); } catch ( Exception $e ) { // Invalid timzeone $tz = null; } if ( ! $tz || $allday || preg_match( "/GMT[+|-][0-9]{4}.*/", $event_timezone ) ) { $event_timezone = $local_timezone; } $start = $this->_time_array_to_datetime( (array)$start['date'], (array)$start['time'], $event_timezone, $feed->import_timezone ? $local_timezone : null ); $end = $this->_time_array_to_datetime( (array)$end['date'], (array)$end['time'], $event_timezone, $feed->import_timezone ? $local_timezone : null ); if ( false === $start || false === $end ) { throw new Ai1ec_Parse_Exception( 'Failed to parse one or more dates given timezone "' . var_export( $event_timezone, true ) . '"' ); continue; } // If all-day, and start and end times are equal, then this event has // invalid end time (happens sometimes with poorly implemented iCalendar // exports, such as in The Event Calendar), so set end time to 1 day // after start time. if ( $allday && $start->format() === $end->format() ) { $end->adjust_day( +1 ); } $data += compact( 'start', 'end', 'allday' ); // ======================================= // = Recurrence rules & recurrence dates = // ======================================= $rrule = $e->rrule; $rdate = $e->rdate; // ======================================= // = Exclusion rules & exclusion dates = // ======================================= $exrule = $e->exrule; $exdate = $e->exdate; // Add custom exclusions if there any $recurrence_id = $e->recurrence_id; if ( '' === $recurrence_id && ! empty( $exclusions[$e->uid] ) ) { if ( isset( $exdate{0} ) ) { $exdate .= ','; } $exdate .= implode( ',', $exclusions[$e->uid] ); } // ======================== // = Latitude & longitude = // ======================== $latitude = $longitude = NULL; $geo_tag = isset( $e->geo ) ? $e->geo : null; if ( ! empty( $geo_tag ) && false !== strpos( $geo_tag, ',' ) ) { list( $latitude, $longitude ) = explode( ',', $geo_tag, 2 ); $latitude = (float)$latitude; $longitude = (float)$longitude; } unset( $geo_tag ); if ( NULL !== $latitude ) { $data += compact( 'latitude', 'longitude' ); // Check the input coordinates checkbox, otherwise lat/long data // is not present on the edit event page $data['show_coordinates'] = 1; } // =================== // = Venue & address = // =================== $data['country'] = ''; $data['city'] = ''; $data['province'] = ''; $data['postal_code'] = ''; $address = ''; $venue = ''; $location = $e->location; $matches = array(); // This regexp matches a venue / address in the format // "venue @ address" or "venue - address". preg_match( '/\s*(.*\S)\s+[\-@]\s+(.*)\s*/', $location, $matches ); // if there is no match, it's not a combined venue + address if ( empty( $matches ) ) { // temporary fix for Mac ICS import. Se AIOEC-2187 // and https://github.com/iCalcreator/iCalcreator/issues/13 $location = str_replace( '\n', "\n", $location ); // if there is a comma, probably it's an address if ( false === strpos( $location, ',' ) ) { $venue = $location; } else { $address = $location; } } else { $venue = isset( $matches[1] ) ? $matches[1] : ''; $address = isset( $matches[2] ) ? $matches[2] : ''; } // ===================================================== // = Set show map status based on presence of location = // ===================================================== $event_do_show_map = $do_show_map; if ( 1 === $do_show_map && NULL === $latitude && empty( $address ) ) { $event_do_show_map = 0; } // ================== // = Cost & tickets = // ================== $cost = $e->x_cost; $ticket_url = $e->x_tickets_url; // =============================== // = Contact name, phone, e-mail = // =============================== // Initialize default values $data['contact_email'] = ''; $data['contact_url'] = ''; $data['contact_phone'] = ''; $data['contact_name'] = ''; $organizer = $e->organizer; if ( 'MAILTO:' === substr( $organizer, 0, 7 ) && false === strpos( $organizer, '@' ) ) { $organizer = substr( $organizer, 7 ); } $contact = $e->contact; $elements = explode( ';', $contact, 4 ); foreach ( $elements as $el ) { $el = trim( $el ); // Detect e-mail address. if ( false !== strpos( $el, '@' ) ) { $data['contact_email'] = $el; } // Detect URL. elseif ( false !== strpos( $el, '://' ) ) { $data['contact_url'] = $el; } // Detect phone number. elseif ( preg_match( '/^[\+0-9\-\(\)\s]*$/', $el ) && strlen( $el ) <= 32 ) { $data['contact_phone'] = $el; } // Default to name. else { $data['contact_name'] = $el; } } if ( ! isset( $data['contact_name'] ) || ! $data['contact_name'] ) { // If no contact name, default to organizer property. $data['contact_name'] = $organizer; } $description = stripslashes( str_replace( '\n', "\n", $e->description )); $description = $this->_remove_ticket_url( $description ); // Store yet-unsaved values to the $data array. $data += array( 'recurrence_rules' => $rrule, 'exception_rules' => $exrule, 'recurrence_dates' => $rdate, 'exception_dates' => $exdate, 'venue' => $venue, 'address' => $address, 'cost' => $cost, 'ticket_url' => $ticket_url, 'show_map' => $event_do_show_map, 'ical_feed_url' => $feed->feed_url, 'ical_source_url' => $e->url, 'ical_organizer' => $organizer, 'ical_contact' => $contact, 'ical_uid' => $this->_get_ical_uid( $e ), 'categories' => array_keys( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] ), 'tags' => array_keys( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] ), 'feed' => $feed, 'post' => array( 'post_status' => 'publish', 'comment_status' => $comment_status, 'post_type' => AI1EC_POST_TYPE, 'post_author' => 1, 'post_title' => $e->summary, 'post_content' => $description ) ); // register any custom exclusions for given event $exclusions = $this->_add_recurring_events_exclusions( $e, $exclusions, $start ); // Create event object. $data = apply_filters( 'ai1ec_pre_init_event_from_feed', $data, $e, $feed ); $event = $this->_registry->get( 'model.event', $data ); // Instant Event $is_instant = $e->x_instant_event; if ( $is_instant ) { $event->set_no_end_time(); } $recurrence = $event->get( 'recurrence_rules' ); $search = $this->_registry->get( 'model.search' ); // first let's check by UID $matching_event_id = $search ->get_matching_event_by_uid_and_url( $event->get( 'ical_uid' ), $event->get( 'ical_feed_url' ) ); // if no result, perform the legacy check. if ( null === $matching_event_id ) { $matching_event_id = $search ->get_matching_event_id( $event->get( 'ical_uid' ), $event->get( 'ical_feed_url' ), $event->get( 'start' ), ! empty( $recurrence ) ); } if ( null === $matching_event_id ) { // ================================================= // = Event was not found, so store it and the post = // ================================================= $event->save(); $count++; } else { // ====================================================== // = Event was found, let's store the new event details = // ====================================================== $uid_cal = $e->uid; if ( ! ai1ec_is_blank( $uid_cal ) ) { $uid_cal_original = sprintf( $event->get_uid_pattern(), $matching_event_id ); if ( $uid_cal_original === $uid_cal ) { //avoiding cycle import //ignore the event, it belongs to site unset( $events_in_db[$matching_event_id] ); continue; } } // Update the post $post = get_post( $matching_event_id ); if ( null !== $post ) { $post->post_title = $event->get( 'post' )->post_title; $post->post_content = $event->get( 'post' )->post_content; wp_update_post( $post ); // Update the event $event->set( 'post_id', $matching_event_id ); $event->set( 'post', $post ); $event->save( true ); $count++; } } do_action( 'ai1ec_ics_event_saved', $event, $feed ); // import not standard taxonomies. unset( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] ); foreach ( $imported_cat as $tax_name => $ids ) { wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name ); } unset( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] ); foreach ( $imported_tags as $tax_name => $ids ) { wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name ); } // import the metadata used by ticket events $cost_type = $e->x_cost_type; if ( $cost_type && false === ai1ec_is_blank( $cost_type ) ) { update_post_meta( $event->get( 'post_id' ), '_ai1ec_cost_type', $cost_type ); } $api_event_id = $e->x_api_event_id; if ( $api_event_id && false === ai1ec_is_blank( $api_event_id ) ) { $api_event_id = $api_event_id; } else { $api_event_id = null; } $api_url = $e->x_api_url; if ( $api_url && false === ai1ec_is_blank( $api_url ) ) { $api_url = $api_url; } else { $api_url = null; } $checkout_url = $e->x_checkout_url; if ( $checkout_url && false === ai1ec_is_blank( $checkout_url ) ) { $checkout_url = $checkout_url; } else { $checkout_url = null; } $currency = $e->x_api_event_currency; if ( ! $currency || ai1ec_is_blank( $currency ) ) { $currency = null; } if ( $api_event_id || $api_url || $checkout_url || $currency ) { if ( ! isset( $api ) ) { $api = $this->_registry->get( 'model.api.api-ticketing' ); } $api->save_api_event_data( $event->get( 'post_id' ), $api_event_id, $api_url, $checkout_url, $currency ); } $wp_images_url = $e->x_wp_image_url; if ( $wp_images_url && false === ai1ec_is_blank( $wp_images_url ) ) { $images_arr = explode( ',', $wp_images_url ); foreach ( $images_arr as $key => $value ) { $images_arr[ $key ] = explode( ';', $value ); } if ( count( $images_arr ) > 0 ) { update_post_meta( $event->get( 'post_id' ), '_featured_image', $images_arr ); } } unset( $events_in_db[$event->get( 'post_id' )] ); } //close while iteration return array( 'count' => $count, 'events_to_delete' => $events_in_db, 'messages' => $messages, 'name' => $feed_name, ); } /** * Returns modified ical uid for google recurring edited events. * * @param vevent $e Vevent object. * * @return string ICAL uid. */ protected function _get_ical_uid( $e ) { $ical_uid = $e->uid; $recurrence_id = $e->recurrence_id; if ( '' !== $recurrence_id ) { $ical_uid = $recurrence_id . '-' . $ical_uid; } return $ical_uid; } /** * time_array_to_timestamp function * * Converts time array to time string. * Passed array: Array( 'year', 'month', 'day', ['hour', 'minute', 'second', ['tz']] ) * Return int: UNIX timestamp in GMT * * @param array $time iCalcreator time property array * (*full* format expected) * @param string $def_timezone Default time zone in case not defined * in $time * @param null|string $forced_timezone Timezone to use instead of UTC. * * @return int UNIX timestamp **/ protected function _time_array_to_datetime( array $date, array $time, $def_timezone, $forced_timezone = null ) { $timezone = $def_timezone; $date_time = $this->_registry->get( 'date.time' ); if ( ! empty( $timezone ) ) { $parser = $this->_registry->get( 'date.timezone' ); $timezone = $parser->get_name( $timezone ); if ( false === $timezone ) { return false; } } // Set default timezone $date_time->set_timezone( $timezone ); if ( ! isset( $time['hour'] ) ) { $time['hour'] = $time['minute'] = $time['second'] = 0; } $date_time ->set_date( $date['year'], $date['month'], $date['day'] ) ->set_time( $time['hour'], $time['minute'], $time['second'] ); // Set choosen timezone if ( null !== $forced_timezone) { $date_time->set_timezone( $forced_timezone ); } return $date_time; } /** * Check if date-time specification has no (empty) time component. * * @param array $datetime Datetime array returned by iCalcreator. * * @return bool Timelessness. */ protected function _is_timeless( array $datetime ) { $timeless = true; foreach ( array( 'hour', 'minute', 'second' ) as $field ) { $timeless &= ( isset( $datetime[$field] ) && 0 != $datetime[$field] ) ? false : true; } return $timeless; } /** * Parse importable feed timezone to sensible value. * * @param string $def_timezone Timezone value from feed. * * @return string Valid timezone name to use. */ protected function _get_import_timezone( $def_timezone ) { $parser = $this->_registry->get( 'date.timezone' ); $timezone = $parser->get_name( $def_timezone ); if ( false === $timezone ) { return 'sys.default'; } return $timezone; } /** * _string_to_datetime function * * Converts time string "Y-m-d H:i:s" to DateTime object. * Passed array: Array( 'year', 'month', 'day', ['hour', 'minute', 'second', ['tz']] ) * Return int: UNIX timestamp in GMT * * @param array $time iCalcreator time property array * (*full* format expected) * @param string $def_timezone Default time zone in case not defined * in $time * @param null|string $forced_timezone Timezone to use instead of UTC. * * @return int UNIX timestamp **/ protected function _string_to_datetime( $time, $def_timezone, $forced_timezone = null ) { $date_time = null; if ( 0 === strcasecmp( $def_timezone, 'utc' ) && null !== $forced_timezone ) { $date_time = $this->_registry->get( 'date.time', $time, $forced_timezone ); } else { $date_time = $this->_registry->get( 'date.time', $time, $def_timezone ); } return $date_time; } /** * Takes a comma-separated list of tags or categories. * If they exist, reuses * the existing ones. If not, creates them. * * The $imported_terms array uses keys to store values rather than values to * speed up lookups (using isset() insted of in_array()). * * @param string $terms * @param array $imported_terms * @param boolean $is_tag * @param boolean $use_name * * @return array */ public function add_categories_and_tags( $terms, array $imported_terms, $is_tag, $use_name ) { $taxonomy = $is_tag ? 'events_tags' : 'events_categories'; $categories = explode( ',', $terms ); $event_taxonomy = $this->_registry->get( 'model.event.taxonomy' ); foreach ( $categories as $cat_name ) { $cat_name = trim( $cat_name ); if ( empty( $cat_name ) ) { continue; } $term = $event_taxonomy->initiate_term( $cat_name, $taxonomy, ! $use_name ); if ( false !== $term ) { if ( ! isset( $imported_terms[$term['taxonomy']] ) ) { $imported_terms[$term['taxonomy']] = array(); } $imported_terms[$term['taxonomy']][$term['term_id']] = true; } } return $imported_terms; } /** * Remove the Ticket URL that maybe exists inside the field Description of the Event */ protected function _remove_ticket_url( $description ) { return preg_replace( '/