iCalendarReader

Gets and renders iCal feeds for the Upcoming Events widget and shortcode.

Defined (1)

The class is defined in the following location(s).

/_inc/lib/icalendar-reader.php  
  1. class iCalendarReader { 
  2.  
  3. public $todo_count = 0; 
  4. public $event_count = 0; 
  5. public $cal = array(); 
  6. public $_lastKeyWord = ''; 
  7. public $timezone = null; 
  8.  
  9. /** 
  10. * Class constructor 
  11. * @return void 
  12. */ 
  13. public function __construct() {} 
  14.  
  15. /** 
  16. * Return an array of events 
  17. * @param string $url (default: '') 
  18. * @return array | false on failure 
  19. */ 
  20. public function get_events( $url = '', $count = 5 ) { 
  21. $count = (int) $count; 
  22. $transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count; 
  23.  
  24. $vcal = get_transient( $transient_id ); 
  25.  
  26. if ( ! empty( $vcal ) ) { 
  27. if ( isset( $vcal['TIMEZONE'] ) ) 
  28. $this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] ); 
  29.  
  30. if ( isset( $vcal['VEVENT'] ) ) { 
  31. $vevent = $vcal['VEVENT']; 
  32.  
  33. if ( $count > 0 ) 
  34. $vevent = array_slice( $vevent, 0, $count ); 
  35.  
  36. $this->cal['VEVENT'] = $vevent; 
  37.  
  38. return $this->cal['VEVENT']; 
  39.  
  40. if ( ! $this->parse( $url ) ) 
  41. return false; 
  42.  
  43. $vcal = array(); 
  44.  
  45. if ( $this->timezone ) { 
  46. $vcal['TIMEZONE'] = $this->timezone->getName(); 
  47. } else { 
  48. $this->timezone = $this->timezone_from_string( '' ); 
  49.  
  50. if ( ! empty( $this->cal['VEVENT'] ) ) { 
  51. $vevent = $this->cal['VEVENT']; 
  52.  
  53. // check for recurring events 
  54. // $vevent = $this->add_recurring_events( $vevent ); 
  55.  
  56. // remove before caching - no sense in hanging onto the past 
  57. $vevent = $this->filter_past_and_recurring_events( $vevent ); 
  58.  
  59. // order by soonest start date 
  60. $vevent = $this->sort_by_recent( $vevent ); 
  61.  
  62. $vcal['VEVENT'] = $vevent; 
  63.  
  64. set_transient( $transient_id, $vcal, HOUR_IN_SECONDS ); 
  65.  
  66. if ( !isset( $vcal['VEVENT'] ) ) 
  67. return false; 
  68.  
  69. if ( $count > 0 ) 
  70. return array_slice( $vcal['VEVENT'], 0, $count ); 
  71.  
  72. return $vcal['VEVENT']; 
  73.  
  74. function apply_timezone_offset( $events ) { 
  75. if ( ! $events ) { 
  76. return $events; 
  77.  
  78. // get timezone offset from the timezone name. 
  79. $timezone_name = get_option( 'timezone_string' ); 
  80. if ( $timezone_name ) { 
  81. $timezone = new DateTimeZone( $timezone_name ); 
  82. } else { 
  83. // If the timezone isn't set then the GMT offset must be set. 
  84. // generate a DateInterval object from the timezone offset 
  85. $gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_MINUTES; 
  86. $timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} minutes" ); 
  87. $timezone = new DateTimeZone( 'UTC' ); 
  88.  
  89. $offsetted_events = array(); 
  90.  
  91. foreach ( $events as $event ) { 
  92. // Don't handle all-day events 
  93. if ( 8 < strlen( $event['DTSTART'] ) ) { 
  94. $start_time = preg_replace( '/Z$/', '', $event['DTSTART'] ); 
  95. $start_time = new DateTime( $start_time, $this->timezone ); 
  96. $start_time->setTimeZone( $timezone ); 
  97.  
  98. $end_time = preg_replace( '/Z$/', '', $event['DTEND'] ); 
  99. $end_time = new DateTime( $end_time, $this->timezone ); 
  100. $end_time->setTimeZone( $timezone ); 
  101.  
  102. if ( $timezone_offset_interval ) { 
  103. $start_time->add( $timezone_offset_interval ); 
  104. $end_time->add( $timezone_offset_interval ); 
  105.  
  106. $event['DTSTART'] = $start_time->format( 'YmdHis\Z' ); 
  107. $event['DTEND'] = $end_time->format( 'YmdHis\Z' ); 
  108.  
  109. $offsetted_events[] = $event; 
  110.  
  111. return $offsetted_events; 
  112.  
  113. protected function filter_past_and_recurring_events( $events ) { 
  114. $upcoming = array(); 
  115. $set_recurring_events = array(); 
  116. $recurrences = array(); 
  117. /** 
  118. * This filter allows any time to be passed in for testing or changing timezones, etc... 
  119. * @module widgets 
  120. *  
  121. * @since 3.4.0 
  122. * @param object time() A time object. 
  123. */ 
  124. $current = apply_filters( 'ical_get_current_time', time() ); 
  125.  
  126. foreach ( $events as $event ) { 
  127.  
  128. $date_from_ics = strtotime( $event['DTSTART'] ); 
  129. if ( isset( $event['DTEND'] ) ) { 
  130. $duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] ); 
  131. } else { 
  132. $duration = 0; 
  133.  
  134. if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) { 
  135. try { 
  136. $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') ); 
  137. $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) ); 
  138. $event['DTSTART'] = $adjusted_time->format('Ymd\THis'); 
  139. $date_from_ics = strtotime( $event['DTSTART'] ); 
  140.  
  141. $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration ); 
  142. } catch ( Exception $e ) { 
  143. // Invalid argument to DateTime 
  144.  
  145. if ( isset( $event['EXDATE'] ) ) { 
  146. $exdates = array(); 
  147. foreach ( (array) $event['EXDATE'] as $exdate ) { 
  148. try { 
  149. $adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') ); 
  150. $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) ); 
  151. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  152. $exdates[] = $adjusted_time->format( 'Ymd' ); 
  153. } else { 
  154. $exdates[] = $adjusted_time->format( 'Ymd\THis' ); 
  155. } catch ( Exception $e ) { 
  156. // Invalid argument to DateTime 
  157. $event['EXDATE'] = $exdates; 
  158. } else { 
  159. $event['EXDATE'] = array(); 
  160.  
  161. if ( ! isset( $event['DTSTART'] ) ) { 
  162. continue; 
  163.  
  164. // Process events with RRULE before other events 
  165. $rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ; 
  166. $uid = $event['UID']; 
  167.  
  168. if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) { 
  169.  
  170. // Break down the RRULE into digestible chunks 
  171. $rrule_array = array(); 
  172.  
  173. foreach ( explode( ";", $event['RRULE'] ) as $rline ) { 
  174. list( $rkey, $rvalue ) = explode( "=", $rline, 2 ); 
  175. $rrule_array[$rkey] = $rvalue; 
  176.  
  177. $interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1; 
  178. $rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0; 
  179. $until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current ); 
  180.  
  181. // Used to bound event checks 
  182. $echo_limit = 10; 
  183. $noop = false; 
  184.  
  185. // Set bydays for the event 
  186. $weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' ); 
  187. $bydays = $weekdays; 
  188.  
  189. // Calculate a recent start date for incrementing depending on the frequency and interval 
  190. switch ( $rrule_array['FREQ'] ) { 
  191.  
  192. case 'DAILY': 
  193. $frequency = 'day'; 
  194. $echo_limit = 10; 
  195.  
  196. if ( $date_from_ics >= $current ) { 
  197. $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) ); 
  198. } else { 
  199. // Interval and count 
  200. $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) ); 
  201. if ( $rrule_count && $catchup > 0 ) { 
  202. if ( $catchup < $rrule_count ) { 
  203. $rrule_count = $rrule_count - $catchup; 
  204. $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  205. } else { 
  206. $noop = true; 
  207. } else { 
  208. $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  209. break; 
  210.  
  211. case 'WEEKLY': 
  212. $frequency = 'week'; 
  213. $echo_limit = 4; 
  214.  
  215. // BYDAY exception to current date 
  216. $day = false; 
  217. if ( ! isset( $rrule_array['BYDAY'] ) ) { 
  218. $day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) ); 
  219. $bydays = explode( ', ', $rrule_array['BYDAY'] ); 
  220.  
  221. if ( $date_from_ics >= $current ) { 
  222. $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) ); 
  223. } else { 
  224. // Interval and count 
  225. $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) ); 
  226. if ( $rrule_count && $catchup > 0 ) { 
  227. if ( ( $catchup * count( $bydays ) ) < $rrule_count ) { 
  228. $rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count 
  229. $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  230. } else { 
  231. $noop = true; 
  232. } else { 
  233. $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  234.  
  235. // Set to Sunday start 
  236. if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) { 
  237. $recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  238. break; 
  239.  
  240. case 'MONTHLY': 
  241. $frequency = 'month'; 
  242. $echo_limit = 1; 
  243.  
  244. if ( $date_from_ics >= $current ) { 
  245. $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) ); 
  246. } else { 
  247. // Describe the date in the month 
  248. if ( isset( $rrule_array['BYDAY'] ) ) { 
  249. $day_number = substr( $rrule_array['BYDAY'], 0, 1 ); 
  250. $week_day = substr( $rrule_array['BYDAY'], 1 ); 
  251. $day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' ); 
  252. $weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' ); 
  253. $event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of "; 
  254. } else { 
  255. $event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) ); 
  256.  
  257. // Interval only 
  258. if ( $interval > 1 ) { 
  259. $catchup = 0; 
  260. $maybe = strtotime( $event['DTSTART'] ); 
  261. while ( $maybe < $current ) { 
  262. $maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ); 
  263. $catchup++; 
  264. $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * ( $catchup - 1 ) ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  265. } else { 
  266. $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  267.  
  268. // Add one interval if necessary 
  269. if ( strtotime( $recurring_event_date_start ) < $current ) { 
  270. if ( $interval > 1 ) { 
  271. $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  272. } else { 
  273. try { 
  274. $adjustment = new DateTime( date( 'Y-m-d', $current ) ); 
  275. $adjustment->modify( 'first day of next month' ); 
  276. $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) ); 
  277. } catch ( Exception $e ) { 
  278. // Invalid argument to DateTime 
  279. break; 
  280.  
  281. case 'YEARLY': 
  282. $frequency = 'year'; 
  283. $echo_limit = 1; 
  284.  
  285. if ( $date_from_ics >= $current ) { 
  286. $recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) ); 
  287. } else { 
  288. $recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) ); 
  289. if ( strtotime( $recurring_event_date_start ) < $current ) { 
  290. try { 
  291. $next = new DateTime( date( 'Y-m-d', $current ) ); 
  292. $next->modify( 'first day of next year' ); 
  293. $recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) ); 
  294. } catch ( Exception $e ) { 
  295. // Invalid argument to DateTime 
  296. break; 
  297.  
  298. default: 
  299. $frequency = false; 
  300.  
  301. if ( $frequency !== false && ! $noop ) { 
  302. $count_counter = 1; 
  303.  
  304. // If no COUNT limit, go to 10 
  305. if ( empty( $rrule_count ) ) { 
  306. $rrule_count = 10; 
  307.  
  308. // Set up EXDATE handling for the event 
  309. $exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array(); 
  310.  
  311. for ( $i = 1; $i <= $echo_limit; $i++ ) { 
  312.  
  313. // Weeks need a daily loop and must check for inclusion in BYDAYS 
  314. if ( 'week' == $frequency ) { 
  315. $byday_event_date_start = strtotime( $recurring_event_date_start ); 
  316.  
  317. foreach ( $weekdays as $day ) { 
  318.  
  319. $event_start_timestamp = $byday_event_date_start; 
  320. $start_time = date( 'His', $event_start_timestamp ); 
  321. $event_end_timestamp = $event_start_timestamp + $duration; 
  322. $end_time = date( 'His', $event_end_timestamp ); 
  323. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  324. $exdate_compare = date( 'Ymd', $event_start_timestamp ); 
  325. } else { 
  326. $exdate_compare = date( 'Ymd\THis', $event_start_timestamp ); 
  327.  
  328. if ( in_array( $day, $bydays ) && $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) { 
  329. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  330. $event['DTSTART'] = date( 'Ymd', $event_start_timestamp ); 
  331. $event['DTEND'] = date( 'Ymd', $event_end_timestamp ); 
  332. } else { 
  333. $event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp ); 
  334. $event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp ); 
  335. if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) { 
  336. try { 
  337. $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) ); 
  338. $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) ); 
  339. $event['DTSTART'] = $adjusted_time->format('Ymd\THis'); 
  340.  
  341. $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration ); 
  342. } catch ( Exception $e ) { 
  343. // Invalid argument to DateTime 
  344. $upcoming[] = $event; 
  345. $count_counter++; 
  346.  
  347. // Move forward one day 
  348. $byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time ); 
  349.  
  350. // Restore first event timestamp 
  351. $event_start_timestamp = strtotime( $recurring_event_date_start ); 
  352.  
  353. } else { 
  354.  
  355. $event_start_timestamp = strtotime( $recurring_event_date_start ); 
  356. $start_time = date( 'His', $event_start_timestamp ); 
  357. $event_end_timestamp = $event_start_timestamp + $duration; 
  358. $end_time = date( 'His', $event_end_timestamp ); 
  359. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  360. $exdate_compare = date( 'Ymd', $event_start_timestamp ); 
  361. } else { 
  362. $exdate_compare = date( 'Ymd\THis', $event_start_timestamp ); 
  363.  
  364. if ( $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) { 
  365. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  366. $event['DTSTART'] = date( 'Ymd', $event_start_timestamp ); 
  367. $event['DTEND'] = date( 'Ymd', $event_end_timestamp ); 
  368. } else { 
  369. $event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time; 
  370. $event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time; 
  371. if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) { 
  372. try { 
  373. $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) ); 
  374. $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) ); 
  375. $event['DTSTART'] = $adjusted_time->format('Ymd\THis'); 
  376.  
  377. $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration ); 
  378. } catch ( Exception $e ) { 
  379. // Invalid argument to DateTime 
  380. $upcoming[] = $event; 
  381. $count_counter++; 
  382.  
  383. // Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact 
  384. $next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp ); 
  385. if ( 8 == strlen( $event['DTSTART'] ) ) { 
  386. $event['DTSTART'] = date( 'Ymd', $next_start_timestamp ); 
  387. $event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration ); 
  388. } else { 
  389. $event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp ); 
  390. $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration ); 
  391.  
  392. // Move recurring event date forward 
  393. $recurring_event_date_start = $event['DTSTART']; 
  394. $set_recurring_events[] = $uid; 
  395.  
  396.  
  397. } else { 
  398. // Process normal events 
  399. if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) { 
  400. $upcoming[] = $event; 
  401. return $upcoming; 
  402.  
  403. /** 
  404. * Parse events from an iCalendar feed 
  405. * @param string $url (default: '') 
  406. * @return array | false on failure 
  407. */ 
  408. public function parse( $url = '' ) { 
  409. $cache_group = 'icalendar_reader_parse'; 
  410. $disable_get_key = 'disable:' . md5( $url ); 
  411.  
  412. // Check to see if previous attempts have failed 
  413. if ( false !== wp_cache_get( $disable_get_key, $cache_group ) ) 
  414. return false; 
  415.  
  416. // rewrite webcal: URI schem to HTTP 
  417. $url = preg_replace('/^webcal/', 'http', $url ); 
  418. // try to fetch 
  419. $r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) ); 
  420. if ( 200 !== wp_remote_retrieve_response_code( $r ) ) { 
  421. // We were unable to fetch any content, so don't try again for another 60 seconds 
  422. wp_cache_set( $disable_get_key, 1, $cache_group, 60 ); 
  423. return false; 
  424.  
  425. $body = wp_remote_retrieve_body( $r ); 
  426. if ( empty( $body ) ) 
  427. return false; 
  428.  
  429. $body = str_replace( "\r\n", "\n", $body ); 
  430. $lines = preg_split( "/\n(?=[A-Z])/", $body ); 
  431.  
  432. if ( empty( $lines ) ) 
  433. return false; 
  434.  
  435. if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) ) 
  436. return false; 
  437.  
  438. foreach ( $lines as $line ) { 
  439. $add = $this->key_value_from_string( $line ); 
  440. if ( ! $add ) { 
  441. $this->add_component( $type, false, $line ); 
  442. continue; 
  443. list( $keyword, $value ) = $add; 
  444.  
  445. switch ( $keyword ) { 
  446. case 'BEGIN': 
  447. case 'END': 
  448. switch ( $line ) { 
  449. case 'BEGIN:VTODO': 
  450. $this->todo_count++; 
  451. $type = 'VTODO'; 
  452. break; 
  453. case 'BEGIN:VEVENT': 
  454. $this->event_count++; 
  455. $type = 'VEVENT'; 
  456. break; 
  457. case 'BEGIN:VCALENDAR': 
  458. case 'BEGIN:DAYLIGHT': 
  459. case 'BEGIN:VTIMEZONE': 
  460. case 'BEGIN:STANDARD': 
  461. $type = $value; 
  462. break; 
  463. case 'END:VTODO': 
  464. case 'END:VEVENT': 
  465. case 'END:VCALENDAR': 
  466. case 'END:DAYLIGHT': 
  467. case 'END:VTIMEZONE': 
  468. case 'END:STANDARD': 
  469. $type = 'VCALENDAR'; 
  470. break; 
  471. break; 
  472. case 'TZID': 
  473. if ( 'VTIMEZONE' == $type && ! $this->timezone ) 
  474. $this->timezone = $this->timezone_from_string( $value ); 
  475. break; 
  476. case 'X-WR-TIMEZONE': 
  477. if ( ! $this->timezone ) 
  478. $this->timezone = $this->timezone_from_string( $value ); 
  479. break; 
  480. default: 
  481. $this->add_component( $type, $keyword, $value ); 
  482. break; 
  483.  
  484. // Filter for RECURRENCE-IDs 
  485. $recurrences = array(); 
  486. if ( array_key_exists( 'VEVENT', $this->cal ) ) { 
  487. foreach ( $this->cal['VEVENT'] as $event ) { 
  488. if ( isset( $event['RECURRENCE-ID'] ) ) { 
  489. $recurrences[] = $event; 
  490. foreach ( $recurrences as $recurrence ) { 
  491. for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) { 
  492. if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) { 
  493. $this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID']; 
  494. break; 
  495.  
  496. return $this->cal; 
  497.  
  498. /** 
  499. * Parse key:value from a string 
  500. * @param string $text (default: '') 
  501. * @return array 
  502. */ 
  503. public function key_value_from_string( $text = '' ) { 
  504. preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches ); 
  505.  
  506. if ( 0 == count( $matches ) ) 
  507. return false; 
  508.  
  509. return array( $matches[1], $matches[3] ); 
  510.  
  511. /** 
  512. * Convert a timezone name into a timezone object. 
  513. * @param string $text Timezone name. Example: America/Chicago 
  514. * @return object|null A DateTimeZone object if the conversion was successful. 
  515. */ 
  516. private function timezone_from_string( $text ) { 
  517. try { 
  518. $timezone = new DateTimeZone( $text ); 
  519. } catch ( Exception $e ) { 
  520. $blog_timezone = get_option( 'timezone_string' ); 
  521. if ( ! $blog_timezone ) { 
  522. $blog_timezone = 'Etc/UTC'; 
  523.  
  524. $timezone = new DateTimeZone( $blog_timezone ); 
  525.  
  526. return $timezone; 
  527.  
  528. /** 
  529. * Add a component to the calendar array 
  530. * @param string $component (default: '') 
  531. * @param string $keyword (default: '') 
  532. * @param string $value (default: '') 
  533. * @return void 
  534. */ 
  535. public function add_component( $component = '', $keyword = '', $value = '' ) { 
  536. if ( false == $keyword ) { 
  537. $keyword = $this->last_keyword; 
  538. switch ( $component ) { 
  539. case 'VEVENT': 
  540. $value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value; 
  541. break; 
  542. case 'VTODO' : 
  543. $value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value; 
  544. break; 
  545.  
  546. /** 
  547. * Some events have a specific timezone set in their start/end date,  
  548. * and it may or may not be different than the calendar timzeone. 
  549. * Valid formats include: 
  550. * DTSTART;TZID=Pacific Standard Time:20141219T180000 
  551. * DTEND;TZID=Pacific Standard Time:20141219T200000 
  552. * EXDATE:19960402T010000Z, 19960403T010000Z, 19960404T010000Z 
  553. * EXDATE;VALUE=DATE:2015050 
  554. * EXDATE;TZID=America/New_York:20150424T170000 
  555. * EXDATE;TZID=Pacific Standard Time:20120615T140000, 20120629T140000, 20120706T140000 
  556. */ 
  557.  
  558. // Always store EXDATE as an array 
  559. if ( stristr( $keyword, 'EXDATE' ) ) { 
  560. $value = explode( ', ', $value ); 
  561.  
  562. // Adjust DTSTART, DTEND, and EXDATE according to their TZID if set 
  563. if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) { 
  564. $keyword = explode( ';', $keyword ); 
  565.  
  566. $tzid = false; 
  567. if ( 2 == count( $keyword ) ) { 
  568. $tparam = $keyword[1]; 
  569.  
  570. if ( strpos( $tparam, "TZID" ) !== false ) { 
  571. $tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) ); 
  572.  
  573. // Normalize all times to default UTC 
  574. if ( $tzid ) { 
  575. $adjusted_times = array(); 
  576. foreach ( (array) $value as $v ) { 
  577. try { 
  578. $adjusted_time = new DateTime( $v, $tzid ); 
  579. $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) ); 
  580. $adjusted_times[] = $adjusted_time->format('Ymd\THis'); 
  581. } catch ( Exception $e ) { 
  582. // Invalid argument to DateTime 
  583. return; 
  584. $value = $adjusted_times; 
  585.  
  586. // Format for adding to event 
  587. $keyword = $keyword[0]; 
  588. if ( 'EXDATE' != $keyword ) { 
  589. $value = implode( (array) $value ); 
  590.  
  591. foreach ( (array) $value as $v ) { 
  592. switch ($component) { 
  593. case 'VTODO': 
  594. if ( 'EXDATE' == $keyword ) { 
  595. $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v; 
  596. } else { 
  597. $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v; 
  598. break; 
  599. case 'VEVENT': 
  600. if ( 'EXDATE' == $keyword ) { 
  601. $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v; 
  602. } else { 
  603. $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v; 
  604. break; 
  605. default: 
  606. $this->cal[ $component ][ $keyword ] = $v; 
  607. break; 
  608. $this->last_keyword = $keyword; 
  609.  
  610. /** 
  611. * Escape strings with wp_kses, allow links 
  612. * @param string $string (default: '') 
  613. * @return string 
  614. */ 
  615. public function escape( $string = '' ) { 
  616. // Unfold content lines per RFC 5545 
  617. $string = str_replace( "\n\t", '', $string ); 
  618. $string = str_replace( "\n ", '', $string ); 
  619.  
  620. $allowed_html = array( 
  621. 'a' => array( 
  622. 'href' => array(),  
  623. 'title' => array() 
  624. ); 
  625.  
  626. $allowed_tags = ''; 
  627. foreach ( array_keys( $allowed_html ) as $tag ) { 
  628. $allowed_tags .= "<{$tag}>"; 
  629.  
  630. // Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc 
  631. // because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra 
  632. // safety and good measure. 
  633. return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html ); 
  634.  
  635. /** 
  636. * Render the events 
  637. * @param string $url (default: '') 
  638. * @param string $context (default: 'widget') or 'shortcode' 
  639. * @return mixed bool|string false on failure, rendered HTML string on success. 
  640. */ 
  641. public function render( $url = '', $args = array() ) { 
  642.  
  643. $args = wp_parse_args( $args, array( 
  644. 'context' => 'widget',  
  645. 'number' => 5 
  646. ) ); 
  647.  
  648. $events = $this->get_events( $url, $args['number'] ); 
  649. $events = $this->apply_timezone_offset( $events ); 
  650.  
  651. if ( empty( $events ) ) 
  652. return false; 
  653.  
  654. ob_start(); 
  655.  
  656. if ( 'widget' == $args['context'] ) : ?> 
  657. <ul class="upcoming-events"> 
  658. <?php foreach ( $events as $event ) : ?> 
  659. <li> 
  660. <strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong> 
  661. <span class="event-when"><?php echo $this->formatted_date( $event ); ?></span> 
  662. <?php if ( ! empty( $event['LOCATION'] ) ) : ?> 
  663. <span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span> 
  664. <?php endif; ?> 
  665. <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?> 
  666. <span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span> 
  667. <?php endif; ?> 
  668. </li> 
  669. <?php endforeach; ?> 
  670. </ul> 
  671. <?php endif; 
  672.  
  673. if ( 'shortcode' == $args['context'] ) : ?> 
  674. <table class="upcoming-events"> 
  675. <thead> 
  676. <tr> 
  677. <th><?php esc_html_e( 'Location', 'jetpack' ); ?></th> 
  678. <th><?php esc_html_e( 'When', 'jetpack' ); ?></th> 
  679. <th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th> 
  680. <th><?php esc_html_e( 'Description', 'jetpack' ); ?></th> 
  681. </tr> 
  682. </thead> 
  683. <tbody> 
  684. <?php foreach ( $events as $event ) : ?> 
  685. <tr> 
  686. <td><?php echo empty( $event['LOCATION'] ) ? ' ' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td> 
  687. <td><?php echo $this->formatted_date( $event ); ?></td> 
  688. <td><?php echo empty( $event['SUMMARY'] ) ? ' ' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td> 
  689. <td><?php echo empty( $event['DESCRIPTION'] ) ? ' ' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td> 
  690. </tr> 
  691. <?php endforeach; ?> 
  692. </tbody> 
  693. </table> 
  694. <?php endif; 
  695.  
  696. $rendered = ob_get_clean(); 
  697.  
  698. if ( empty( $rendered ) ) 
  699. return false; 
  700.  
  701. return $rendered; 
  702.  
  703. public function formatted_date( $event ) { 
  704.  
  705. $date_format = get_option( 'date_format' ); 
  706. $time_format = get_option( 'time_format' ); 
  707. $start = strtotime( $event['DTSTART'] ); 
  708. $end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false; 
  709.  
  710. $all_day = ( 8 == strlen( $event['DTSTART'] ) ); 
  711.  
  712. if ( !$all_day && $this->timezone ) { 
  713. try { 
  714. $start_time = new DateTime( $event['DTSTART'] ); 
  715. $timezone_offset = $this->timezone->getOffset( $start_time ); 
  716. $start += $timezone_offset; 
  717.  
  718. if ( $end ) { 
  719. $end += $timezone_offset; 
  720. } catch ( Exception $e ) { 
  721. // Invalid argument to DateTime 
  722. $single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true; 
  723.  
  724. /** Translators: Date and time */ 
  725. $date_with_time = __( '%1$s at %2$s' , 'jetpack' ); 
  726. /** Translators: Two dates with a separator */ 
  727. $two_dates = __( '%1$s – %2$s' , 'jetpack' ); 
  728.  
  729. // we'll always have the start date. Maybe with time 
  730. if ( $all_day ) 
  731. $date = date_i18n( $date_format, $start ); 
  732. else 
  733. $date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) ); 
  734.  
  735. // single day, timed 
  736. if ( $single_day && ! $all_day && false !== $end ) 
  737. $date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) ); 
  738.  
  739. // multi-day 
  740. if ( ! $single_day ) { 
  741.  
  742. if ( $all_day ) { 
  743. // DTEND for multi-day events represents "until", not "including", so subtract one minute 
  744. $end_date = date_i18n( $date_format, $end - 60 ); 
  745. } else { 
  746. $end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) ); 
  747.  
  748. $date = sprintf( $two_dates, $date, $end_date ); 
  749.  
  750.  
  751. return $date; 
  752.  
  753. protected function sort_by_recent( $list ) { 
  754. $dates = $sorted_list = array(); 
  755.  
  756. foreach ( $list as $key => $row ) { 
  757. $date = $row['DTSTART']; 
  758. // pad some time onto an all day date 
  759. if ( 8 === strlen( $date ) ) 
  760. $date .= 'T000000Z'; 
  761. $dates[$key] = $date; 
  762. asort( $dates ); 
  763. foreach( $dates as $key => $value ) { 
  764. $sorted_list[$key] = $list[$key]; 
  765. unset($list); 
  766. return $sorted_list; 
  767.