nicolapeluchetti

ics.php

Jan 8th, 2015
223
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 34.57 KB | None | 0 0
  1. <?php
  2.  
  3. /**
  4.  * The ics import/export engine.
  5.  *
  6.  * @author     Time.ly Network Inc.
  7.  * @since      2.0
  8.  *
  9.  * @package    AI1EC
  10.  * @subpackage AI1EC.Import-export
  11.  */
  12. class Ai1ec_Ics_Import_Export_Engine
  13.     extends Ai1ec_Base
  14.     implements Ai1ec_Import_Export_Engine {
  15.  
  16.     /**
  17.      * @var Ai1ec_Taxonomy
  18.      */
  19.     protected $_taxonomy_model = null;
  20.  
  21.     /* (non-PHPdoc)
  22.      * @see Ai1ec_Import_Export_Engine::import()
  23.      */
  24.     public function import( array $arguments ) {
  25.         $cal = $this->_registry->get( 'vcalendar' );
  26.         if ( $cal->parse( $arguments['source'] ) ) {
  27.             $count = 0;
  28.             try {
  29.                 $result = $this->add_vcalendar_events_to_db(
  30.                     $cal,
  31.                     $arguments
  32.                 );
  33.             } catch ( Ai1ec_Parse_Exception $exception ) {
  34.                 throw new Ai1ec_Parse_Exception(
  35.                     'Processing "' . $arguments['source'] .
  36.                     '" triggered error: ' . $exception->getMessage()
  37.                 );
  38.             }
  39.             return $result;
  40.         }
  41.         throw new Ai1ec_Parse_Exception( 'The passed string is not a valid ics feed' );
  42.     }
  43.  
  44.     /* (non-PHPdoc)
  45.      * @see Ai1ec_Import_Export_Engine::export()
  46.      */
  47.     public function export( array $arguments, array $params = array() ) {
  48.         $c = new vcalendar();
  49.         $c->setProperty( 'calscale', 'GREGORIAN' );
  50.         $c->setProperty( 'method', 'PUBLISH' );
  51.         // if no post id are specified do not export those properties
  52.         // as they would create a new calendar in outlook.
  53.         // a user reported this in AIOEC-982 and said this would fix it
  54.         if( true === $arguments['do_not_export_as_calendar'] ) {
  55.             $c->setProperty( 'X-WR-CALNAME', get_bloginfo( 'name' ) );
  56.             $c->setProperty( 'X-WR-CALDESC', get_bloginfo( 'description' ) );
  57.         }
  58.         $c->setProperty( 'X-FROM-URL', home_url() );
  59.         // Timezone setup
  60.         $tz = $this->_registry->get( 'date.timezone' )->get_default_timezone();
  61.         if ( $tz ) {
  62.             $c->setProperty( 'X-WR-TIMEZONE', $tz );
  63.             $tz_xprops = array( 'X-LIC-LOCATION' => $tz );
  64.             iCalUtilityFunctions::createTimezone( $c, $tz, $tz_xprops );
  65.         }
  66.  
  67.         $this->_taxonomy_model = $this->_registry->get( 'model.taxonomy' );
  68.         $post_ids = array();
  69.         foreach ( $arguments['events'] as $event ) {
  70.             $post_ids[] = $event->get( 'post_id' );
  71.         }
  72.         $this->_taxonomy_model->prepare_meta_for_ics( $post_ids );
  73.         $this->_registry->get( 'controller.content-filter' )
  74.             ->clear_the_content_filters();
  75.         foreach ( $arguments['events'] as $event ) {
  76.             $c = $this->_insert_event_in_calendar(
  77.                 $event,
  78.                 $c,
  79.                 true,
  80.                 $params
  81.             );
  82.         }
  83.         $this->_registry->get( 'controller.content-filter' )
  84.             ->restore_the_content_filters();
  85.         $str = ltrim( $c->createCalendar() );
  86.         return $str;
  87.     }
  88.  
  89.     /**
  90.      * Check if date-time specification has no (empty) time component.
  91.      *
  92.      * @param array $datetime Datetime array returned by iCalcreator.
  93.      *
  94.      * @return bool Timelessness.
  95.      */
  96.     protected function _is_timeless( array $datetime ) {
  97.         $timeless = true;
  98.         foreach ( array( 'hour', 'min', 'sec' ) as $field ) {
  99.             $timeless &= (
  100.                     isset( $datetime[$field] ) &&
  101.                     0 != $datetime[$field]
  102.             )
  103.             ? false
  104.             : true;
  105.         }
  106.         return $timeless;
  107.     }
  108.  
  109.     /**
  110.      * Process vcalendar instance - add events to database.
  111.      *
  112.      * @param vcalendar $v    Calendar to retrieve data from.
  113.      * @param array     $args Arbitrary arguments map.
  114.      *
  115.      * @throws Ai1ec_Parse_Exception
  116.      *
  117.      * @internal param stdClass $feed           Instance of feed (see Ai1ecIcs plugin).
  118.      * @internal param string   $comment_status WP comment status: 'open' or 'closed'.
  119.      * @internal param int      $do_show_map    Map display status (DB boolean: 0 or 1).
  120.      *
  121.      * @return int Count of events added to database.
  122.      */
  123.     public function add_vcalendar_events_to_db(
  124.         vcalendar $v,
  125.         array $args
  126.     ) {
  127.         $feed           = isset( $args['feed'] ) ? $args['feed'] : null;
  128.         $comment_status = isset( $args['comment_status'] ) ? $args['comment_status'] : 'open';
  129.         $do_show_map    = isset( $args['do_show_map'] ) ? $args['do_show_map'] : 0;
  130.         $count = 0;
  131.         $events_in_db   = isset( $args['events_in_db'] ) ? $args['events_in_db'] : 0;
  132.         $v->sort();
  133.         // Reverse the sort order, so that RECURRENCE-IDs are listed before the
  134.         // defining recurrence events, and therefore take precedence during
  135.         // caching.
  136.         $v->components = array_reverse( $v->components );
  137.  
  138.         // TODO: select only VEVENT components that occur after, say, 1 month ago.
  139.         // Maybe use $v->selectComponents(), which takes into account recurrence
  140.  
  141.         // Fetch default timezone in case individual properties don't define it
  142.         $tz = $v->getComponent( 'vtimezone' );
  143.         if ( ! empty( $tz ) ) {
  144.             $timezone   = $tz->getProperty( 'TZID' );
  145.         }
  146.         if ( empty( $timezone ) ) {
  147.             $timezone   = $v->getProperty( 'X-WR-TIMEZONE' );
  148.             $timezone   = (string)$timezone[1];
  149.         }
  150.  
  151.         $messages       = array();
  152.         $local_timezone = $this->_registry->get( 'date.timezone' )
  153.             ->get_default_timezone();
  154.         $current_timestamp = $this->_registry->get( 'date.time' )->format_to_gmt();
  155.         // initialize empty custom exclusions structure
  156.         $exclusions        = array();
  157.         // go over each event
  158.         while ( $e = $v->getComponent( 'vevent' ) ) {
  159.             // Event data array.
  160.             $data = array();
  161.             // =====================
  162.             // = Start & end times =
  163.             // =====================
  164.             $start = $e->getProperty( 'dtstart', 1, true );
  165.             $end   = $e->getProperty( 'dtend',   1, true );
  166.             // For cases where a "VEVENT" calendar component
  167.             // specifies a "DTSTART" property with a DATE value type but none
  168.             // of "DTEND" nor "DURATION" property, the event duration is taken to
  169.             // be one day.  For cases where a "VEVENT" calendar component
  170.             // specifies a "DTSTART" property with a DATE-TIME value type but no
  171.             // "DTEND" property, the event ends on the same calendar date and
  172.             // time of day specified by the "DTSTART" property.
  173.             if ( empty( $end ) )  {
  174.                 // #1 if duration is present, assign it to end time
  175.                 $end = $e->getProperty( 'duration', 1, true, true );
  176.                 if ( empty( $end ) ) {
  177.                     // #2 if only DATE value is set for start, set duration to 1 day
  178.                     if ( ! isset( $start['value']['hour'] ) ) {
  179.                         $end = array(
  180.                             'value' => array(
  181.                                 'year'  => $start['value']['year'],
  182.                                 'month' => $start['value']['month'],
  183.                                 'day'   => $start['value']['day'] + 1,
  184.                                 'hour'  => 0,
  185.                                 'min'   => 0,
  186.                                 'sec'   => 0,
  187.                             ),
  188.                         );
  189.                         if ( isset( $start['value']['tz'] ) ) {
  190.                             $end['value']['tz'] = $start['value']['tz'];
  191.                         }
  192.                     } else {
  193.                         // #3 set end date to start time
  194.                         $end = $start;
  195.                     }
  196.                 }
  197.             }
  198.  
  199.             $categories = $e->getProperty( "CATEGORIES", false, true );
  200.             $imported_cat = array( Ai1ec_Event_Taxonomy::CATEGORIES => array() );
  201.             // If the user chose to preserve taxonomies during import, add categories.
  202.             if( $categories && $feed->keep_tags_categories ) {
  203.                 $imported_cat = $this->_add_categories_and_tags(
  204.                         $categories['value'],
  205.                         $imported_cat,
  206.                         false,
  207.                         true
  208.                 );
  209.             }
  210.             $feed_categories = $feed->feed_category;
  211.             if( ! empty( $feed_categories ) ) {
  212.                 $imported_cat = $this->_add_categories_and_tags(
  213.                         $feed_categories,
  214.                         $imported_cat,
  215.                         false,
  216.                         false
  217.                 );
  218.             }
  219.             $tags = $e->getProperty( "X-TAGS", false, true );
  220.  
  221.             $imported_tags = array( Ai1ec_Event_Taxonomy::TAGS => array() );
  222.             // If the user chose to preserve taxonomies during import, add tags.
  223.             if( $tags && $feed->keep_tags_categories ) {
  224.                 $imported_tags = $this->_add_categories_and_tags(
  225.                         $tags[1]['value'],
  226.                         $imported_tags,
  227.                         true,
  228.                         true
  229.                 );
  230.             }
  231.             $feed_tags = $feed->feed_tags;
  232.             if( ! empty( $feed_tags ) ) {
  233.                 $imported_tags = $this->_add_categories_and_tags(
  234.                         $feed_tags,
  235.                         $imported_tags,
  236.                         true,
  237.                         true
  238.                 );
  239.             }
  240.             // Event is all-day if no time components are defined
  241.             $allday = $this->_is_timeless( $start['value'] ) &&
  242.                 $this->_is_timeless( $end['value'] );
  243.             // Also check the proprietary MS all-day field.
  244.             $ms_allday = $e->getProperty( 'X-MICROSOFT-CDO-ALLDAYEVENT' );
  245.             if ( ! empty( $ms_allday ) && $ms_allday[1] == 'TRUE' ) {
  246.                 $allday = true;
  247.             }
  248.             $event_timezone = $timezone;
  249.             if ( $allday ) {
  250.                 $event_timezone = $local_timezone;
  251.             }
  252.             $start = $this->_time_array_to_datetime( $start, $event_timezone );
  253.             $end   = $this->_time_array_to_datetime( $end,   $event_timezone );
  254.  
  255.             if ( false === $start || false === $end ) {
  256.                 throw new Ai1ec_Parse_Exception(
  257.                     'Failed to parse one or more dates given timezone "' .
  258.                     var_export( $event_timezone, true ) . '"'
  259.                 );
  260.                 continue;
  261.             }
  262.  
  263.             // If all-day, and start and end times are equal, then this event has
  264.             // invalid end time (happens sometimes with poorly implemented iCalendar
  265.             // exports, such as in The Event Calendar), so set end time to 1 day
  266.             // after start time.
  267.             if ( $allday && $start->format() === $end->format() ) {
  268.                 $end->adjust_day( +1 );
  269.             }
  270.  
  271.             $data += compact( 'start', 'end', 'allday' );
  272.  
  273.             // =======================================
  274.             // = Recurrence rules & recurrence dates =
  275.             // =======================================
  276.             if ( $rrule = $e->createRrule() ) {
  277.                 $rrule = explode( ':', $rrule );
  278.                 $rrule = trim( end( $rrule ) );
  279.             }
  280.  
  281.             if ( $exrule = $e->createExrule() ) {
  282.                 $exrule = explode( ':', $exrule );
  283.                 $exrule = trim( end( $exrule ) );
  284.             }
  285.  
  286.             if ( $rdate = $e->createRdate() ) {
  287.                 $rdate = explode( ':', $rdate );
  288.                 $rdate = trim( end( $rdate ) );
  289.             }
  290.  
  291.             // ===================
  292.             // = Exception dates =
  293.             // ===================
  294.             $exdate = '';
  295.             if ( $exdates = $e->createExdate() ){
  296.                 // We may have two formats:
  297.                 // one exdate with many dates ot more EXDATE rules
  298.                 $exdates      = explode( 'EXDATE', $exdates );
  299.                 $def_timezone = $this->_get_import_timezone( $event_timezone );
  300.                 foreach ( $exdates as $exd ) {
  301.                     if ( empty( $exd ) ) {
  302.                         continue;
  303.                     }
  304.                     $exploded       = explode( ':', $exd );
  305.                     $excpt_timezone = $def_timezone;
  306.                     $excpt_date     = null;
  307.                     foreach ( $exploded as $particle ) {
  308.                         if ( ';TZID=' === substr( $particle, 0, 6 ) ) {
  309.                             $excpt_timezone = substr( $particle, 6 );
  310.                         } else {
  311.                             $excpt_date = trim( $particle );
  312.                         }
  313.                     }
  314.                     // Google sends YYYYMMDD for all-day excluded events
  315.                     if (
  316.                         $allday &&
  317.                         8 === strlen( $excpt_date )
  318.                     ) {
  319.                         $excpt_date    .= 'T000000Z';
  320.                         $excpt_timezone = 'UTC';
  321.                     }
  322.                     $ex_dt = $this->_registry->get(
  323.                         'date.time',
  324.                         $excpt_date,
  325.                         $excpt_timezone
  326.                     );
  327.                     if ( $ex_dt ) {
  328.                         if ( isset( $exdate{0} ) ) {
  329.                             $exdate .= ',';
  330.                         }
  331.                         $exdate .= $ex_dt->format( 'Ymd\THis', $excpt_timezone );
  332.                     }
  333.                 }
  334.             }
  335.             // Add custom exclusions if there any
  336.             $recurrence_id = $e->getProperty( 'recurrence-id' );
  337.             if (
  338.                 false === $recurrence_id &&
  339.                 ! empty( $exclusions[$e->getProperty( 'uid' )] )
  340.             ) {
  341.                 if ( isset( $exdate{0} ) ) {
  342.                     $exdate .= ',';
  343.                 }
  344.                 $exdate .= implode( ',', $exclusions[$e->getProperty( 'uid' )] );
  345.             }
  346.             // ========================
  347.             // = Latitude & longitude =
  348.             // ========================
  349.             $latitude = $longitude = NULL;
  350.             $geo_tag  = $e->getProperty( 'geo' );
  351.             if ( is_array( $geo_tag ) ) {
  352.                 if (
  353.                 isset( $geo_tag['latitude'] ) &&
  354.                 isset( $geo_tag['longitude'] )
  355.                 ) {
  356.                     $latitude  = (float)$geo_tag['latitude'];
  357.                     $longitude = (float)$geo_tag['longitude'];
  358.                 }
  359.             } else if ( ! empty( $geo_tag ) && false !== strpos( $geo_tag, ';' ) ) {
  360.                 list( $latitude, $longitude ) = explode( ';', $geo_tag, 2 );
  361.                 $latitude  = (float)$latitude;
  362.                 $longitude = (float)$longitude;
  363.             }
  364.             unset( $geo_tag );
  365.             if ( NULL !== $latitude ) {
  366.                 $data += compact( 'latitude', 'longitude' );
  367.                 // Check the input coordinates checkbox, otherwise lat/long data
  368.                 // is not present on the edit event page
  369.                 $data['show_coordinates'] = 1;
  370.             }
  371.  
  372.             // ===================
  373.             // = Venue & address =
  374.             // ===================
  375.             $address = $venue = '';
  376.             $location = $e->getProperty( 'location' );
  377.             $matches = array();
  378.             // This regexp matches a venue / address in the format
  379.             // "venue @ address" or "venue - address".
  380.             preg_match( '/\s*(.*\S)\s+[\-@]\s+(.*)\s*/', $location, $matches );
  381.             // if there is no match, it's not a combined venue + address
  382.             if ( empty( $matches ) ) {
  383.                 // if there is a comma, probably it's an address
  384.                 if ( false === strpos( $location, ',' ) ) {
  385.                     $venue = $location;
  386.                 } else {
  387.                     $address = $location;
  388.                 }
  389.             } else {
  390.                 $venue = isset( $matches[1] ) ? $matches[1] : '';
  391.                 $address = isset( $matches[2] ) ? $matches[2] : '';
  392.             }
  393.  
  394.             // =====================================================
  395.             // = Set show map status based on presence of location =
  396.             // =====================================================
  397.             if (
  398.                 1 === $do_show_map &&
  399.                 NULL === $latitude &&
  400.                 empty( $address )
  401.             ) {
  402.                 $do_show_map = 0;
  403.             }
  404.  
  405.             // ==================
  406.             // = Cost & tickets =
  407.             // ==================
  408.             $cost       = $e->getProperty( 'X-COST' );
  409.             $cost       = $cost ? $cost[1] : '';
  410.             $ticket_url = $e->getProperty( 'X-TICKETS-URL' );
  411.             $ticket_url = $ticket_url ? $ticket_url[1] : '';
  412.  
  413.             // ===============================
  414.             // = Contact name, phone, e-mail =
  415.             // ===============================
  416.             $organizer = $e->getProperty( 'organizer' );
  417.             if (
  418.                 'MAILTO:' === substr( $organizer, 0, 7 ) &&
  419.                 false === strpos( $organizer, '@' )
  420.             ) {
  421.                 $organizer = substr( $organizer, 7 );
  422.             }
  423.             $contact = $e->getProperty( 'contact' );
  424.             $elements = explode( ';', $contact, 4 );
  425.             foreach ( $elements as $el ) {
  426.                 $el = trim( $el );
  427.                 // Detect e-mail address.
  428.                 if ( false !== strpos( $el, '@' ) ) {
  429.                     $data['contact_email'] = $el;
  430.                 }
  431.                 // Detect URL.
  432.                 elseif ( false !== strpos( $el, '://' ) ) {
  433.                     $data['contact_url']   = $this->_parse_legacy_loggable_url(
  434.                         $el
  435.                     );
  436.                 }
  437.                 // Detect phone number.
  438.                 elseif ( preg_match( '/\d/', $el ) ) {
  439.                     $data['contact_phone'] = $el;
  440.                 }
  441.                 // Default to name.
  442.                 else {
  443.                     $data['contact_name']  = $el;
  444.                 }
  445.             }
  446.             if ( ! isset( $data['contact_name'] ) || ! $data['contact_name'] ) {
  447.                 // If no contact name, default to organizer property.
  448.                 $data['contact_name']    = $organizer;
  449.             }
  450.             // Store yet-unsaved values to the $data array.
  451.             $data += array(
  452.                 'recurrence_rules'  => $rrule,
  453.                 'exception_rules'   => $exrule,
  454.                 'recurrence_dates'  => $rdate,
  455.                 'exception_dates'   => $exdate,
  456.                 'venue'             => $venue,
  457.                 'address'           => $address,
  458.                 'cost'              => $cost,
  459.                 'ticket_url'        => $this->_parse_legacy_loggable_url(
  460.                     $ticket_url
  461.                 ),
  462.                 'show_map'          => $do_show_map,
  463.                 'ical_feed_url'     => $feed->feed_url,
  464.                 'ical_source_url'   => $e->getProperty( 'url' ),
  465.                 'ical_organizer'    => $organizer,
  466.                 'ical_contact'      => $contact,
  467.                 'ical_uid'          => $this->_get_ical_uid( $e ),
  468.                 'categories'        => array_keys( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] ),
  469.                 'tags'              => array_keys( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] ),
  470.                 'feed'              => $feed,
  471.                 'post'              => array(
  472.                     'post_status'       => 'publish',
  473.                         'comment_status'    => $comment_status,
  474.                         'post_type'         => AI1EC_POST_TYPE,
  475.                         'post_author'       => 1,
  476.                         'post_title'        => $e->getProperty( 'summary' ),
  477.                         'post_content'      => stripslashes(
  478.                             str_replace(
  479.                                 '\n',
  480.                                 "\n",
  481.                                 $e->getProperty( 'description' )
  482.                             )
  483.                         ),
  484.                 ),
  485.             );
  486.             // register any custom exclusions for given event
  487.             $exclusions = $this->_add_recurring_events_exclusions(
  488.                 $e,
  489.                 $exclusions,
  490.                 $start
  491.             );
  492.  
  493.             // Create event object.
  494.             $data  = apply_filters(
  495.                 'ai1ec_pre_init_event_from_feed',
  496.                 $data,
  497.                 $e,
  498.                 $feed
  499.             );
  500.  
  501.             $event = $this->_registry->get( 'model.event', $data );
  502.  
  503.             // Instant Event
  504.             $is_instant = $e->getProperty( 'X-INSTANT-EVENT' );
  505.             if ( $is_instant ) {
  506.                 $event->set_no_end_time();
  507.             }
  508.  
  509.             $recurrence = $event->get( 'recurrence_rules' );
  510.             $search = $this->_registry->get( 'model.search' );
  511.             // first let's check by UID
  512.             $matching_event_id = $search
  513.                 ->get_matching_event_by_uid_and_url(
  514.                     $event->get( 'ical_uid' ),
  515.                     $event->get( 'ical_feed_url' )
  516.                 );
  517.             // if no result, perform the legacy check.
  518.             if ( null === $matching_event_id ) {
  519.                 $matching_event_id = $search
  520.                     ->get_matching_event_id(
  521.                         $event->get( 'ical_uid' ),
  522.                         $event->get( 'ical_feed_url' ),
  523.                         $event->get( 'start' ),
  524.                         ! empty( $recurrence )
  525.                     );
  526.             }
  527.             if ( null === $matching_event_id ) {
  528.                 // =================================================
  529.                 // = Event was not found, so store it and the post =
  530.                 // =================================================
  531.                     $event->save();
  532.                     $count++;
  533.             } else {
  534.                 // ======================================================
  535.                 // = Event was found, let's store the new event details =
  536.                 // ======================================================
  537.  
  538.                 // Update the post
  539.                 $post               = get_post( $matching_event_id );
  540.  
  541.                 if ( null !== $post ) {
  542.                     $post->post_title   = $event->get( 'post' )->post_title;
  543.                     $post->post_content = $event->get( 'post' )->post_content;
  544.                     wp_update_post( $post );
  545.  
  546.                     // Update the event
  547.                     $event->set( 'post_id', $matching_event_id );
  548.                     $event->set( 'post',    $post );
  549.                     $event->save( true );
  550.                     $count++;
  551.                 }
  552.  
  553.             }
  554.  
  555.             // import not standard taxonomies.
  556.             unset( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] );
  557.             foreach ( $imported_cat as $tax_name => $ids ) {
  558.                 wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
  559.             }
  560.  
  561.             unset( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] );
  562.             foreach ( $imported_tags as $tax_name => $ids ) {
  563.                 wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
  564.             }
  565.        
  566.             // if the event is not finished, unset it otherwise it could be deleted afterwards.
  567.             if ( $event->get( 'end' )->format_to_gmt() > $current_timestamp ) {
  568.                 unset( $events_in_db[$event->get( 'post_id' )] );
  569.             }
  570.         }
  571.  
  572.         return array(
  573.             'count'            => $count,
  574.             'events_to_delete' => $events_in_db,
  575.             'messages'         => $messages,
  576.         );
  577.     }
  578.  
  579.     /**
  580.      * Convert loggable URL exported from legacy Ai1EC installation.
  581.      *
  582.      * @param string $loggable_url Likely loggable URL.
  583.      *
  584.      * @return string Non-loggable URL.
  585.      */
  586.     protected function _parse_legacy_loggable_url( $loggable_url ) {
  587.         if ( 0 !== strpos( $loggable_url, AI1EC_REDIRECTION_SERVICE ) ) {
  588.             return $loggable_url; // it wasn't loggable URL
  589.         }
  590.         $value = base64_decode(
  591.             substr( $loggable_url, strlen( AI1EC_REDIRECTION_SERVICE ) )
  592.         );
  593.         $clear_url = null; // return empty if nothing is parseable
  594.         if ( // valid JSON structure remains
  595.             null !== ( $decoded = json_decode( $value, true ) ) &&
  596.             isset( $decoded['l'] )
  597.         ) {
  598.             $clear_url = $decoded['l'];
  599.         } else if ( preg_match( '|"l"\s*:\s*"(.+?)","|', $value, $matches ) ) {
  600.             // reverting to dirty parsing as JSON is broken
  601.             $clear_url = stripslashes( $matches[1] );
  602.         } // no more else - impossible to parse anything
  603.         return $clear_url;
  604.     }
  605.  
  606.     /**
  607.      * Parse importable feed timezone to sensible value.
  608.      *
  609.      * @param string $def_timezone Timezone value from feed.
  610.      *
  611.      * @return string Valid timezone name to use.
  612.      */
  613.     protected function _get_import_timezone( $def_timezone ) {
  614.         $parser   = $this->_registry->get( 'date.timezone' );
  615.         $timezone = $parser->get_name( $def_timezone );
  616.         if ( false === $timezone ) {
  617.             return 'sys.default';
  618.         }
  619.         return $timezone;
  620.     }
  621.  
  622.     /**
  623.      * time_array_to_timestamp function
  624.      *
  625.      * Converts time array to time string.
  626.      * Passed array: Array( 'year', 'month', 'day', ['hour', 'min', 'sec', ['tz']] )
  627.      * Return int: UNIX timestamp in GMT
  628.      *
  629.      * @param array  $time         iCalcreator time property array (*full* format expected)
  630.      * @param string $def_timezone Default time zone in case not defined in $time
  631.      *
  632.      * @return int UNIX timestamp
  633.      **/
  634.     protected function _time_array_to_datetime( array $time, $def_timezone ) {
  635.         $timezone = '';
  636.         if ( isset( $time['params']['TZID'] ) ) {
  637.             $timezone = $time['params']['TZID'];
  638.         } elseif (
  639.                 isset( $time['value']['tz'] ) &&
  640.                 'Z' === $time['value']['tz']
  641.         ) {
  642.             $timezone = 'UTC';
  643.         }
  644.         if ( empty( $timezone ) ) {
  645.             $timezone = $def_timezone;
  646.         }
  647.  
  648.         $date_time = $this->_registry->get( 'date.time' );
  649.  
  650.         if ( ! empty( $timezone ) ) {
  651.             $parser   = $this->_registry->get( 'date.timezone' );
  652.             $timezone = $parser->get_name( $timezone );
  653.             if ( false === $timezone ) {
  654.                 return false;
  655.             }
  656.             $date_time->set_timezone( $timezone );
  657.         }
  658.  
  659.         if ( ! isset( $time['value']['hour'] ) ) {
  660.             $time['value']['hour'] = $time['value']['min'] =
  661.                 $time['value']['sec'] = 0;
  662.         }
  663.  
  664.         $date_time->set_date(
  665.             $time['value']['year'],
  666.             $time['value']['month'],
  667.             $time['value']['day']
  668.         )->set_time(
  669.             $time['value']['hour'],
  670.             $time['value']['min'],
  671.             $time['value']['sec']
  672.         );
  673.  
  674.         return $date_time;
  675.     }
  676.  
  677.     /**
  678.      * Convert an event from a feed into a new Ai1ec_Event object and add it to
  679.      * the calendar.
  680.      *
  681.      * @param Ai1ec_Event $event    Event object.
  682.      * @param vcalendar   $calendar Calendar object.
  683.      * @param bool        $export   States whether events are created for export.
  684.      * @param array       $params   Additional parameters for export.
  685.      *
  686.      * @return void
  687.      */
  688.     protected function _insert_event_in_calendar(
  689.             Ai1ec_Event $event,
  690.             vcalendar $calendar,
  691.             $export = false,
  692.             array $params = array()
  693.     ) {
  694.  
  695.         $tz  = $this->_registry->get( 'date.timezone' )
  696.             ->get_default_timezone();
  697.  
  698.         $e   = & $calendar->newComponent( 'vevent' );
  699.         $uid = '';
  700.         if ( $event->get( 'ical_uid' ) ) {
  701.             $uid = addcslashes( $event->get( 'ical_uid' ), "\\;,\n" );
  702.         } else {
  703.             $uid = $event->get_uid();
  704.             $event->set( 'ical_uid', $uid );
  705.             $event->save( true );
  706.         }
  707.         $e->setProperty( 'uid', $this->_sanitize_value( $uid ) );
  708.         $e->setProperty(
  709.             'url',
  710.             get_permalink( $event->get( 'post_id' ) )
  711.         );
  712.  
  713.         // =========================
  714.         // = Summary & description =
  715.         // =========================
  716.         $e->setProperty(
  717.             'summary',
  718.             $this->_sanitize_value(
  719.                 html_entity_decode(
  720.                     apply_filters( 'the_title', $event->get( 'post' )->post_title ),
  721.                     ENT_QUOTES,
  722.                     'UTF-8'
  723.                 )
  724.             )
  725.         );
  726.  
  727.         $content = apply_filters(
  728.             'ai1ec_the_content',
  729.             apply_filters(
  730.                 'the_content',
  731.                 $event->get( 'post' )->post_content
  732.             )
  733.         );
  734.         $content = str_replace(']]>', ']]&gt;', $content);
  735.         $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
  736.  
  737.         // Prepend featured image if available.
  738.         $size = null;
  739.         $avatar = $this->_registry->get( 'view.event.avatar' );
  740.         $matches = $avatar->get_image_from_content( $content );
  741.         // if no img is already present - add thumbnail
  742.         if ( empty( $matches ) ) {
  743.             if ( $img_url = $avatar->get_post_thumbnail_url( $event, $size ) ) {
  744.                 $content = '<div class="ai1ec-event-avatar alignleft timely"><img src="' .
  745.                     esc_attr( $img_url ) . '" width="' . $size[0] . '" height="' .
  746.                     $size[1] . '" /></div>' . $content;
  747.             }
  748.         }
  749.  
  750.         if ( isset( $params['no_html'] ) && $params['no_html'] ) {
  751.             $e->setProperty(
  752.                 'description',
  753.                 $this->_sanitize_value(
  754.                     strip_tags( strip_shortcodes( $content ) )
  755.                 )
  756.             );
  757.             if ( ! empty( $content ) ) {
  758.                 $html_content = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">\n' .
  759.                     '<HTML>\n<HEAD>\n<TITLE></TITLE>\n</HEAD>\n<BODY>' . $content .
  760.                     '</BODY></HTML>';
  761.                 $e->setProperty(
  762.                     'X-ALT-DESC',
  763.                     $this->_sanitize_value( $html_content ),
  764.                     array(
  765.                         'FMTTYPE' => 'text/html',
  766.                     )
  767.                 );
  768.                 unset( $html_content );
  769.             }
  770.         } else {
  771.             $e->setProperty( 'description', $this->_sanitize_value( $content ) );
  772.         }
  773.         $revision = (int)current(
  774.             array_keys(
  775.                 wp_get_post_revisions( $event->get( 'post_id' ) )
  776.             )
  777.         );
  778.         $e->setProperty( 'sequence', $revision );
  779.  
  780.         // =====================
  781.         // = Start & end times =
  782.         // =====================
  783.         $dtstartstring = '';
  784.         $dtstart = $dtend = array();
  785.         if ( $event->is_allday() ) {
  786.             $dtstart['VALUE'] = $dtend['VALUE'] = 'DATE';
  787.             // For exporting all day events, don't set a timezone
  788.             if ( $tz && ! $export ) {
  789.                 $dtstart['TZID'] = $dtend['TZID'] = $tz;
  790.             }
  791.  
  792.             // For exportin' all day events, only set the date not the time
  793.             if ( $export ) {
  794.                 $e->setProperty(
  795.                     'dtstart',
  796.                     $this->_sanitize_value(
  797.                         $event->get( 'start' )->format( 'Ymd' )
  798.                     ),
  799.                     $dtstart
  800.                 );
  801.                 $e->setProperty(
  802.                     'dtend',
  803.                     $this->_sanitize_value(
  804.                         $event->get( 'end' )->format( 'Ymd' )
  805.                     ),
  806.                     $dtend
  807.                 );
  808.             } else {
  809.                 $e->setProperty(
  810.                     'dtstart',
  811.                     $this->_sanitize_value(
  812.                         $event->get( 'start' )->format( "Ymd\T" )
  813.                     ),
  814.                     $dtstart
  815.                 );
  816.                 $e->setProperty(
  817.                     'dtend',
  818.                     $this->_sanitize_value(
  819.                         $event->get( 'end' )->format( "Ymd\T" )
  820.                     ),
  821.                     $dtend
  822.                 );
  823.             }
  824.         } else {
  825.             if ( $tz ) {
  826.                 $dtstart['TZID'] = $dtend['TZID'] = $tz;
  827.             }
  828.             // This is used later.
  829.             $dtstartstring = $event->get( 'start' )->format( "Ymd\THis" );
  830.             $e->setProperty(
  831.                 'dtstart',
  832.                 $this->_sanitize_value( $dtstartstring ),
  833.                 $dtstart
  834.             );
  835.  
  836.  
  837.             $e->setProperty(
  838.                 'dtend',
  839.                 $this->_sanitize_value(
  840.                     $event->get( 'end' )->format( "Ymd\THis" )
  841.                 ),
  842.                 $dtend
  843.             );
  844.         }
  845.  
  846.         // ========================
  847.         // = Latitude & longitude =
  848.         // ========================
  849.         if (
  850.             floatval( $event->get( 'latitude' ) ) ||
  851.             floatval( $event->get( 'longitude' ) )
  852.         ) {
  853.             $e->setProperty(
  854.                 'geo',
  855.                 $event->get( 'latitude' ),
  856.                 $event->get( 'longitude' )
  857.             );
  858.         }
  859.  
  860.         // ===================
  861.         // = Venue & address =
  862.         // ===================
  863.         if ( $event->get( 'venue' ) || $event->get( 'address' ) ) {
  864.             $location = array(
  865.                 $event->get( 'venue' ),
  866.                 $event->get( 'address' )
  867.             );
  868.             $location = array_filter( $location );
  869.             $location = implode( ' @ ', $location );
  870.             $e->setProperty( 'location', $this->_sanitize_value( $location ) );
  871.         }
  872.  
  873.         $categories = array();
  874.         $language   = get_bloginfo( 'language' );
  875.  
  876.         foreach (
  877.             $this->_taxonomy_model->get_post_categories(
  878.                 $event->get( 'post_id' )
  879.             )
  880.             as $cat
  881.         ) {
  882.             $categories[] = $cat->name;
  883.         }
  884.         $e->setProperty(
  885.             'categories',
  886.             implode( ',', $categories ),
  887.             array( "LANGUAGE" => $language )
  888.         );
  889.         $tags = array();
  890.         foreach (
  891.             $this->_taxonomy_model->get_post_tags( $event->get( 'post_id' ) )
  892.             as $tag
  893.         ) {
  894.             $tags[] = $tag->name;
  895.         }
  896.         if( ! empty( $tags) ) {
  897.             $e->setProperty(
  898.                 'X-TAGS',
  899.                 implode( ',', $tags ),
  900.                 array( "LANGUAGE" => $language )
  901.             );
  902.         }
  903.         // ==================
  904.         // = Cost & tickets =
  905.         // ==================
  906.         if ( $event->get( 'cost' ) ) {
  907.             $e->setProperty(
  908.                 'X-COST',
  909.                 $this->_sanitize_value( $event->get( 'cost' ) )
  910.             );
  911.         }
  912.         if ( $event->get( 'ticket_url' ) ) {
  913.             $e->setProperty(
  914.                 'X-TICKETS-URL',
  915.                 $this->_sanitize_value(
  916.                     $event->get_nonloggable_url( $event->get( 'ticket_url' ) )
  917.                 )
  918.             );
  919.         }
  920.         // =================
  921.         // = Instant Event =
  922.         // =================
  923.         if ( $event->is_instant() ) {
  924.             $e->setProperty(
  925.                 'X-INSTANT-EVENT',
  926.                 $this->_sanitize_value( $event->is_instant() )
  927.             );
  928.         }
  929.  
  930.         // ====================================
  931.         // = Contact name, phone, e-mail, URL =
  932.         // ====================================
  933.         $contact = array(
  934.             $event->get( 'contact_name' ),
  935.             $event->get( 'contact_phone' ),
  936.             $event->get( 'contact_email' ),
  937.             $event->get_nonloggable_url( $event->get( 'contact_url' ) ),
  938.         );
  939.         $contact = array_filter( $contact );
  940.         $contact = implode( '; ', $contact );
  941.         $e->setProperty( 'contact', $this->_sanitize_value( $contact ) );
  942.  
  943.         // ====================
  944.         // = Recurrence rules =
  945.         // ====================
  946.         $rrule = array();
  947.         $recurrence = $event->get( 'recurrence_rules' );
  948.         if ( ! empty( $recurrence ) ) {
  949.             $rules = array();
  950.             foreach ( explode( ';', $event->get( 'recurrence_rules' ) ) as $v) {
  951.                 if ( strpos( $v, '=' ) === false ) {
  952.                     continue;
  953.                 }
  954.  
  955.                 list( $k, $v ) = explode( '=', $v );
  956.                 $k = strtoupper( $k );
  957.                 // If $v is a comma-separated list, turn it into array for iCalcreator
  958.                 switch ( $k ) {
  959.                     case 'BYSECOND':
  960.                     case 'BYMINUTE':
  961.                     case 'BYHOUR':
  962.                     case 'BYDAY':
  963.                     case 'BYMONTHDAY':
  964.                     case 'BYYEARDAY':
  965.                     case 'BYWEEKNO':
  966.                     case 'BYMONTH':
  967.                     case 'BYSETPOS':
  968.                         $exploded = explode( ',', $v );
  969.                         break;
  970.                     default:
  971.                         $exploded = $v;
  972.                         break;
  973.                 }
  974.                 // iCalcreator requires a more complex array structure for BYDAY...
  975.                 if ( $k == 'BYDAY' ) {
  976.                     $v = array();
  977.                     foreach ( $exploded as $day ) {
  978.                         $v[] = array( 'DAY' => $day );
  979.                     }
  980.                 } else {
  981.                     $v = $exploded;
  982.                 }
  983.                 $rrule[ $k ] = $v;
  984.             }
  985.         }
  986.  
  987.         // ===================
  988.         // = Exception rules =
  989.         // ===================
  990.         $exceptions = $event->get( 'exception_rules' );
  991.         $exrule = array();
  992.         if ( ! empty( $exceptions ) ) {
  993.             $rules = array();
  994.  
  995.             foreach ( explode( ';', $exceptions ) as $v) {
  996.                 if ( strpos( $v, '=' ) === false ) {
  997.                     continue;
  998.                 }
  999.  
  1000.                 list($k, $v) = explode( '=', $v );
  1001.                 $k = strtoupper( $k );
  1002.                 // If $v is a comma-separated list, turn it into array for iCalcreator
  1003.                 switch ( $k ) {
  1004.                     case 'BYSECOND':
  1005.                     case 'BYMINUTE':
  1006.                     case 'BYHOUR':
  1007.                     case 'BYDAY':
  1008.                     case 'BYMONTHDAY':
  1009.                     case 'BYYEARDAY':
  1010.                     case 'BYWEEKNO':
  1011.                     case 'BYMONTH':
  1012.                     case 'BYSETPOS':
  1013.                         $exploded = explode( ',', $v );
  1014.                         break;
  1015.                     default:
  1016.                         $exploded = $v;
  1017.                         break;
  1018.                 }
  1019.                 // iCalcreator requires a more complex array structure for BYDAY...
  1020.                 if ( $k == 'BYDAY' ) {
  1021.                     $v = array();
  1022.                     foreach ( $exploded as $day ) {
  1023.                         $v[] = array( 'DAY' => $day );
  1024.                     }
  1025.                 } else {
  1026.                     $v = $exploded;
  1027.                 }
  1028.                 $exrule[ $k ] = $v;
  1029.             }
  1030.         }
  1031.  
  1032.         // add rrule to exported calendar
  1033.         if ( ! empty( $rrule ) ) {
  1034.             $e->setProperty( 'rrule', $this->_sanitize_value( $rrule ) );
  1035.         }
  1036.         // add exrule to exported calendar
  1037.         if ( ! empty( $exrule ) ) {
  1038.             $e->setProperty( 'exrule', $this->_sanitize_value( $exrule ) );
  1039.         }
  1040.  
  1041.         // ===================
  1042.         // = Exception dates =
  1043.         // ===================
  1044.         // For all day events that use a date as DTSTART, date must be supplied
  1045.         // For other other events which use DATETIME, we must use that as well
  1046.         // We must also match the exact starting time
  1047.         $exception_dates = $event->get( 'exception_dates' );
  1048.         if ( ! empty( $exception_dates ) ) {
  1049.             $params    = array(
  1050.                 'VALUE' => 'DATE-TIME',
  1051.                 'TZID'  => $tz,
  1052.             );
  1053.             $dt_suffix = $event->get( 'start' )->format( '\THis' );
  1054.             foreach (
  1055.                 explode( ',', $exception_dates )
  1056.                 as $exdate
  1057.             ) {
  1058.                 // date-time string in EXDATES is formatted as 'Ymd\THis\Z', that
  1059.                 // means - in UTC timezone, thus we use `format_to_gmt` here.
  1060.                 $exdate = $this->_registry->get( 'date.time', $exdate )
  1061.                     ->format_to_gmt( 'Ymd' );
  1062.                 $e->setProperty(
  1063.                     'exdate',
  1064.                     array( $exdate . $dt_suffix ),
  1065.                     $params
  1066.                 );
  1067.             }
  1068.         }
  1069.         return $calendar;
  1070.     }
  1071.  
  1072.     /**
  1073.      * _sanitize_value method
  1074.      *
  1075.      * Convert value, so it be safe to use on ICS feed. Used before passing to
  1076.      * iCalcreator methods, for rendering.
  1077.      *
  1078.      * @param string $value Text to be sanitized
  1079.      *
  1080.      * @return string Safe value, for use in HTML
  1081.      */
  1082.     protected function _sanitize_value( $value ) {
  1083.         if ( ! is_scalar( $value ) ) {
  1084.             return $value;
  1085.         }
  1086.         $safe_eol = "\n";
  1087.         $value    = strtr(
  1088.                 trim( $value ),
  1089.                 array(
  1090.                     "\r\n" => $safe_eol,
  1091.                     "\r"   => $safe_eol,
  1092.                     "\n"   => $safe_eol,
  1093.                 )
  1094.         );
  1095.         $value = addcslashes( $value, '\\' );
  1096.         return $value;
  1097.     }
  1098.  
  1099.     /**
  1100.      * Takes a comma-separated list of tags or categories.
  1101.      * If they exist, reuses
  1102.      * the existing ones. If not, creates them.
  1103.      *
  1104.      * The $imported_terms array uses keys to store values rather than values to
  1105.      * speed up lookups (using isset() insted of in_array()).
  1106.      *
  1107.      * @param string  $terms
  1108.      * @param array   $imported_terms
  1109.      * @param boolean $is_tag
  1110.      * @param boolean $use_name
  1111.      *
  1112.      * @return array
  1113.      */
  1114.     protected function _add_categories_and_tags(
  1115.         $terms,
  1116.         array $imported_terms,
  1117.         $is_tag,
  1118.         $use_name
  1119.     ) {
  1120.         $taxonomy       = $is_tag ? 'events_tags' : 'events_categories';
  1121.         $categories     = explode( ',', $terms );
  1122.         $event_taxonomy = $this->_registry->get( 'model.event.taxonomy' );
  1123.  
  1124.         foreach ( $categories as $cat_name ) {
  1125.             $cat_name = trim( $cat_name );
  1126.             if ( empty( $cat_name ) ) {
  1127.                 continue;
  1128.             }
  1129.             $term = $event_taxonomy->initiate_term( $cat_name, $taxonomy, ! $use_name );
  1130.             if ( false !== $term ) {
  1131.                 if ( ! isset( $imported_terms[$term['taxonomy']] ) ) {
  1132.                     $imported_terms[$term['taxonomy']] = array();
  1133.                 }
  1134.                 $imported_terms[$term['taxonomy']][$term['term_id']] = true;
  1135.             }
  1136.         }
  1137.         return $imported_terms;
  1138.     }
  1139.  
  1140.     /**
  1141.      * Returns modified ical uid for google recurring edited events.
  1142.      *
  1143.      * @param vevent $e Vevent object.
  1144.      *
  1145.      * @return string ICAL uid.
  1146.      */
  1147.     protected function _get_ical_uid( $e ) {
  1148.         $ical_uid      = $e->getProperty( 'uid' );
  1149.         $recurrence_id = $e->getProperty( 'recurrence-id' );
  1150.         if ( false !== $recurrence_id ) {
  1151.             $ical_uid = implode( '', array_values( $recurrence_id ) ) . '-' .
  1152.                 $ical_uid;
  1153.         }
  1154.  
  1155.         return $ical_uid;
  1156.     }
  1157.  
  1158.     /**
  1159.      * Returns modified exclusions structure for given event.
  1160.      *
  1161.      * @param vcalendar       $e          Vcalendar event object.
  1162.      * @param array           $exclusions Exclusions.
  1163.      * @param Ai1ec_Date_Time $start Date time object.
  1164.      *
  1165.      * @return array Modified exclusions structure.
  1166.      */
  1167.     protected function _add_recurring_events_exclusions( $e, $exclusions, $start ) {
  1168.         $recurrence_id = $e->getProperty( 'recurrence-id' );
  1169.         if (
  1170.             false === $recurrence_id ||
  1171.             ! isset( $recurrence_id['year'] ) ||
  1172.             ! isset( $recurrence_id['month'] ) ||
  1173.             ! isset( $recurrence_id['day'] )
  1174.         ) {
  1175.             return $exclusions;
  1176.         }
  1177.         $year = $month = $day = $hour = $min = $sec = null;
  1178.         extract( $recurrence_id, EXTR_IF_EXISTS );
  1179.         $timezone = '';
  1180.         $exdate   = sprintf( '%04d%02d%02d', $year, $month, $day );
  1181.         if (
  1182.             null === $hour ||
  1183.             null === $min ||
  1184.             null === $sec
  1185.         ) {
  1186.             $hour = $min = $sec = '00';
  1187.             $timezone = 'Z';
  1188.         }
  1189.         $exdate .= sprintf(
  1190.             'T%02d%02d%02d%s',
  1191.             $hour,
  1192.             $min,
  1193.             $sec,
  1194.             $timezone
  1195.         );
  1196.         $exclusions[$e->getProperty( 'uid' )][] = $exdate;
  1197.         return $exclusions;
  1198.     }
  1199.  
  1200. }
Add Comment
Please, Sign In to add comment