* Invision Power Services * IP.Board v3.4.5 * Library to facilitate iCalendar input and output * Last Updated: $Date: 2013-02-14 18:02:12 -0500 (Thu, 14 Feb 2013) $ * * * @author $Author: bfarber $ * @copyright (c) 2001 - 2009 Invision Power Services, Inc. * @license http://www.invisionpower.com/company/standards.php#license * @package IP.Board * @subpackage Calendar * @link http://www.invisionpower.com * @since 5th January 2005 * @version $Revision: 11991 $ */ if ( ! defined( 'IN_IPB' ) ) { print "

Incorrect access

You cannot access this file directly."; exit(); } class app_calendar_classes_icalendarOutput { /**#@+ * Registry objects * * @access protected * @var object */ protected $registry; protected $DB; protected $settings; protected $request; protected $lang; protected $member; protected $memberData; protected $cache; protected $caches; /**#@-*/ /** * Calendar data * * @var array */ protected $calendar = array(); /** * Calendar events to insert in feed * * @var array */ protected $_events = array(); /** * Calendar functions * * @var object */ protected $functions; /** * Error message from import * * @var string */ protected $_error = ''; /** * Type of begin block we are currently parsing * * @var string */ protected $_begin = ''; /** * Raw iCalendar feed data after parsing * * @var array */ protected $_parsedIcsData = array(); /** * Raw iCalendar data before parsing * * @var array */ protected $_rawIcsData = array(); /** * Temp: Current timezone ID we are parsing * * @var string */ protected $_tzId = ''; /** * Temp: Current timezone ID we are beginning to parse * * @var string */ protected $_tzBegin = ''; /** * Earliest timestamp from feed * * @var int */ protected $_feedEarliest = 0; /** * Latest timestamp from feed * * @var int */ protected $_feedLatest = 0; /** * Constructor * * @param object Registry object * @param int Calendar we want to output * @return @e void */ public function __construct( ipsRegistry $registry, $calendarId ) { /* Make object */ $this->registry = $registry; $this->DB = $this->registry->DB(); $this->settings =& $this->registry->fetchSettings(); $this->request =& $this->registry->fetchRequest(); $this->lang = $this->registry->getClass('class_localization'); $this->member = $this->registry->member(); $this->memberData =& $this->registry->member()->fetchMemberData(); $this->cache = $this->registry->cache(); $this->caches =& $this->registry->cache()->fetchCaches(); //----------------------------------------- // Language files //----------------------------------------- $this->registry->class_localization->loadLanguageFile( array( 'public_calendar' ), 'calendar' ); //----------------------------------------- // Functions class //----------------------------------------- $classToLoad = IPSLib::loadLibrary( IPSLib::getAppDir( 'calendar' ) . "/sources/functions.php", 'app_calendar_classes_functions', 'calendar' ); $this->functions = new $classToLoad( $this->registry, true ); //----------------------------------------- // Get our calendar //----------------------------------------- $this->calendar = $this->functions->getCalendar( $calendarId ); } /** * Add an event * * @param array Event data * @return @e void */ public function addEvent( $event ) { if( $event['event']['event_id'] ) { $this->_events[ $event['event']['event_id'] ] = $event; } } /** * Remove an event * * @param int Event id * @return @e void */ public function removeEvent( $eventId ) { if( $eventId ) { unset($this->_events[ $eventId ]); } } /** * Return events to be output * * @return @e void */ public function getEvents() { return $this->_events; } /** * Build iCalendar feed and return * * @return string iCalendar feed (can be downloaded or sent as webcal subscription) */ public function buildICalendarFeed() { //----------------------------------------- // Start of output //----------------------------------------- $output = "BEGIN:VCALENDAR\r\n"; $output .= "VERSION:2.0\r\n"; $output .= "PRODID:-//IP.Board Calendar " . ipsRegistry::$version . "//EN\r\n"; $output .= "METHOD:PUBLISH\r\n"; $output .= "CALSCALE:GREGORIAN\r\n"; $output .= "X-WR-CALNAME:" . $this->_encodeSpecialCharacters( $this->calendar['cal_title'] ) . "\r\n"; //----------------------------------------- // Format the time zones (GMT only) //----------------------------------------- $output .= $this->_addTimezones(); //----------------------------------------- // Format the events //----------------------------------------- $output .= $this->_addEvents(); //----------------------------------------- // End of output //----------------------------------------- $output .= "END:VCALENDAR\r\n"; //----------------------------------------- // Return the output //----------------------------------------- return $output; } /** * Build the VEVENT parts of the iCalendar feed * * @return string */ protected function _addEvents() { //----------------------------------------- // Init //----------------------------------------- $output = ''; //----------------------------------------- // Loop over events //----------------------------------------- $events = $this->getEvents(); if( count($events) ) { foreach( $events as $event ) { //----------------------------------------- // Basic output stuff //----------------------------------------- $output .= "BEGIN:VEVENT\r\n"; $output .= "SUMMARY:" . $this->_encodeSpecialCharacters( $event['event']['event_title'] ) . "\r\n"; $output .= "DTSTAMP:" . $this->_buildIcalDate( $event['event']['event_saved'] ) . "\r\n"; $output .= "SEQUENCE:" . $event['event']['event_sequence'] . "\r\n"; $output .= "UID:" . $this->_buildUid( array( $event['event']['event_saved'], $event['event']['event_id'] ) ) . "\r\n"; $output .= $this->_foldLines( "ORGANIZER;CN=\"" . $this->_encodeSpecialCharacters( $event['member']['members_display_name'], false ) . '":' . $this->settings['email_out'] ) . "\r\n"; //----------------------------------------- // Attachments... //----------------------------------------- $attachments = array(); if( $event['event']['event_attach_content'] ) { preg_match_all( "/(http.+?attach_id=(\d+))/i", $event['event']['event_attach_content'], $matches ); if( is_array($matches) AND count($matches) ) { foreach( $matches[2] as $k => $v ) { $attachments[ $v ] = $v; } } } preg_match_all( "/(http.+?attach_id=(\d+))/i", $event['event']['event_content'], $matches ); if( is_array($matches) AND count($matches) ) { foreach( $matches[2] as $k => $v ) { $attachments[ $v ] = $v; } } if( count($attachments) ) { $this->DB->build( array( 'select' => '*', 'from' => 'attachments', 'where' => 'attach_id IN(' . implode( ',', $attachments ) . ')' ) ); $this->DB->execute(); while( $r = $this->DB->fetch() ) { $output .= "ATTACH;FMTTYPE=" . $this->caches['attachtypes'][ $r['attach_ext'] ]['atype_mimetype'] . ":" . $this->registry->output->buildSEOUrl( "app=core&module=attach§ion=attach&attach_id=" . $r['attach_id'], 'public' ) . "\r\n"; } } //----------------------------------------- // Output description/content //----------------------------------------- $output .= "DESCRIPTION:" . $this->_encodeSpecialCharacters( $event['event']['event_content'] ) . "\r\n"; //----------------------------------------- // Figure out times (the hard part...) //----------------------------------------- $_startTime = @strtotime( $event['event']['event_start_date'] ); $_endTime = @strtotime( $event['event']['event_end_date'] ); $_allDay = $event['event']['event_all_day']; if( $event['event']['event_recurring'] ) { if( $event['event_all_day'] ) { $output .= "DTSTART;VALUE=DATE:" . $this->_buildIcalDate( $_startTime, true ) . "\r\n"; $output .= "DTEND;VALUE=DATE:" . $this->_buildIcalDate( $_startTime + 86400, true ) . "\r\n"; } else { $output .= "DTSTART;TZID=Europe/London:" . $this->_buildIcalDate( $_startTime ) . "\r\n"; $output .= "DTEND;TZID=Europe/London:" . $this->_buildIcalDate( $_startTime ) . "\r\n"; } } else if( $_endTime ) { if( $_allDay ) { $output .= "DTSTART;VALUE=DATE:" . $this->_buildIcalDate( $_startTime, true ) . "\r\n"; $output .= "DTEND;VALUE=DATE:" . $this->_buildIcalDate( $_endTime + 86400, true ) . "\r\n"; } else { $output .= "DTSTART;TZID=Europe/London:" . $this->_buildIcalDate( $_startTime ) . "\r\n"; $output .= "DTEND;TZID=Europe/London:" . $this->_buildIcalDate( $_endTime ) . "\r\n"; } } else { if( $_allDay ) { $output .= "DTSTART;VALUE=DATE:" . $this->_buildIcalDate( $_startTime, true ) . "\r\n"; } else { $output .= "DTSTART;TZID=Europe/London:" . $this->_buildIcalDate( $_startTime ) . "\r\n"; } } //----------------------------------------- // Recurring //----------------------------------------- if ( $event['event']['event_recurring'] ) { $_freq = $event['event']['event_recurring'] == 3 ? 'YEARLY' : ( $event['event']['event_recurring'] == 2 ? 'MONTHLY' : 'WEEKLY' ); $output .= "RRULE:FREQ=" . $_freq . ";INTERVAL=1;UNTIL=" . $this->_buildIcalDate( $_endTime ) . "\r\n"; } //----------------------------------------- // Attendees? //----------------------------------------- if( is_array($event['event']['_rsvp_attendees']) AND count($event['event']['_rsvp_attendees']) ) { foreach( $event['event']['_rsvp_attendees'] as $mid => $member ) { $output .= $this->_foldLines( "ATTENDEE;CN=\"" . $this->_encodeSpecialCharacters( $member['members_display_name'], false ) . '";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:' . $this->settings['email_out'] ) . "\r\n"; } } $output .= "END:VEVENT\r\n"; } } //----------------------------------------- // Return the output //----------------------------------------- return $output; } /** * Build the VTIMEZONE parts of the iCalendar feed * * @return string */ protected function _addTimezones() { //----------------------------------------- // Init //----------------------------------------- $output = ''; //----------------------------------------- // Get the needed data //----------------------------------------- $events = $this->getEvents(); $years = array(); if( count($events) ) { foreach( $events as $event ) { $_startTime = @strtotime( $event['event']['event_start_date'] ); $_endTime = @strtotime( $event['event']['event_end_date'] ); $_year = gmdate( 'Y', $_startTime ); $years[ $_year ] = $_year; if( $_endTime ) { while( $_startTime < $_endTime ) { $_year = gmdate( 'Y', $_startTime ); $years[ $_year ] = $_year; $_year = gmdate( 'Y', $_endTime ); $years[ $_year ] = $_year; $_startTime += 2592000; // add one month } } } } //----------------------------------------- // Add the timezones to the object //----------------------------------------- foreach( $years as $year ) { $_daylight_start = strtotime( 'last Sunday of March ' . $year ); $_standard_start = strtotime( 'last Sunday of October ' . $year ); $_daylight = gmmktime( 2, 0, 0, 3 , gmdate( 'j', $_daylight_start ), $year ); $_standard = gmmktime( 2, 0, 0, 10, gmdate( 'j', $_standard_start ), $year ); $output .= "BEGIN:VTIMEZONE\r\n"; $output .= "TZID:Europe/London\r\n"; $output .= "TZURL:http://tzurl.org/zoneinfo/Europe/London\r\n"; $output .= "X-LIC-LOCATION:Europe/London\r\n"; $output .= "BEGIN:DAYLIGHT\r\n"; $output .= "TZOFFSETFROM:+0000\r\n"; $output .= "TZOFFSETTO:+0100\r\n"; $output .= "TZNAME:BST\r\n"; $output .= "DTSTART:" . $this->_buildIcalDate( $_daylight ) . "\r\n"; $output .= "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\n"; $output .= "END:DAYLIGHT\r\n"; $output .= "BEGIN:STANDARD\r\n"; $output .= "TZOFFSETFROM:+0100\r\n"; $output .= "TZOFFSETTO:+0000\r\n"; $output .= "TZNAME:GMT\r\n"; $output .= "DTSTART:" . $this->_buildIcalDate( $_standard ) . "\r\n"; $output .= "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\n"; $output .= "END:STANDARD\r\n"; $output .= "END:VTIMEZONE\r\n"; } //----------------------------------------- // Return the output //----------------------------------------- return $output; } /** * Return a UID for iCalendar * * @param array Parameters to use to generate UID * @param string UID */ protected function _buildUid( $params ) { return md5( implode( '.', $params ) ) . '@' . $_SERVER['SERVER_ADDR']; } /** * Return a date in iCalendar format * * @param int Timestamp * @param bool Build date only format * @param string Date in format of YYYYMMDDTHHMMSSZ OR YYYYMMDD */ protected function _buildIcalDate( $time, $dateOnly=false ) { if( $dateOnly ) { return gmdate( 'Ymd', $time ); } else { return gmdate( 'Ymd\THis\Z', $time ); } } /** * Encode special characters in a string for iCalendar * * @param string String to encode * @param bool Line-fold * @param string Encoded string */ protected function _encodeSpecialCharacters( $text, $lineFold=true ) { $text = strip_tags( IPSText::br2nl( $text ) ); $text = str_replace( "\\", "\\\\", $text ); $text = str_replace( "\n" , '\\n', $text ); $text = str_replace( "\r" , '\\n', $text ); $text = str_replace( ',' , '\,', $text ); $text = str_replace( ';' , '\;', $text ); //$text = str_replace( ':' , '\:', $text ); $text = str_replace( '"', '\"', $text ); if( $lineFold ) { $text = $this->_foldLines( $text ); } return $text; } /** * Un-encode special characters in a string coming from iCalendar feed * * @param string String to unencode * @param string Encoded string * @link http://community.invisionpower.com/tracker/issue-33787-formatting-problem-with-imported-ics-file-calendar-app/ */ protected function _unencodeSpecialCharacters( $text ) { /* Reverse encoding */ if( stripos( $text, 'encoding=' ) === 0 ) { preg_match( "#encoding=(.+?):(.+?)$#i", $text, $matches ); if( $matches[1] ) { switch( strtolower($matches[1]) ) { case 'base64': $text = base64_decode( $matches[2] ); break; case 'quoted-printable': $text = quoted_printable_decode( $matches[2] ); break; } } else { $text = substr( $text, strpos( $text, ':' ) ); } } $text = str_replace( '\\n', "\n", $text ); $text = str_replace( '\,', "," , $text ); $text = str_replace( '\;', ";" , $text ); $text = str_replace( '\:', ":" , $text ); $text = str_replace( 'DQUOTE', '"' , $text ); return $text; } /** * Fold lines per RFC2445 * * @param string $text String to fold * @return string * @link https://gist.github.com/81747 */ protected function _foldLines( $text ) { /* Do we have mb functions? */ if ( function_exists('mb_list_encodings') AND function_exists('mb_substr') AND function_exists('mb_strlen') ) { $valid_encodings = mb_list_encodings(); if ( count($valid_encodings) ) { /* Does mb functions cover our charset? */ if ( in_array( strtoupper(IPS_DOC_CHAR_SET), $valid_encodings ) ) { mb_internal_encoding( strtoupper(IPS_DOC_CHAR_SET) ); $return = array(); $_extra = 15; /* Takes into account line beginning, i.e. "DESCRIPTION:" */ while( strlen($text) > 60 ) { $space = 75 - $_extra; /* Remove line beginning - subsequent loops this will be tab character */ $mbcc = $space; while( $mbcc ) { $line = mb_substr( $text, 0, $mbcc ); /* Get first chunk of chars */ $octet = strlen( $line ); /* Determine how long this really is (3-byte letters could triple the size) */ /* Too long ? */ if( $octet > $space ) { $mbcc -= $octet - $space; } else { $return[] = $line; $_extra = 1; $text = mb_substr( $text, $mbcc ); break; } } } /* Anything left? */ if( !empty($text) ) { $return[] = $text; } /* Return now */ return implode( "\r\n\t", $return ); } } } /* Use regular string functions - can break mb characters however, but if mb charset is used, mb functions should be enabled anyways */ return implode( "\r\n\t", str_split( $text, 60 ) ); } /** * Unfold lines per RFC2445 4.1 * * @param string $string Starting string * @param int $line Starting line number * @param string */ protected function _unfoldLines( $string, $line ) { //----------------------------------------- // Recursively unfold as needed //----------------------------------------- if( isset( $this->_rawIcsData[ $line + 1 ] ) AND ( substr( $this->_rawIcsData[ $line + 1 ], 0, 1 ) == ' ' OR substr( $this->_rawIcsData[ $line + 1 ], 0, 1 ) == "\t" ) ) { $string .= ltrim( $this->_rawIcsData[ $line + 1 ] ); $string = $this->_unfoldLines( $string, $line + 1 ); } return $string; } /** * Unparse time information from iCalendar datetime info * * @param string $string iCalendar line * @return array Time information */ protected function _unparseTimeInfo( $string ) { if( strpos( $string, '=' ) !== false ) { $_tmp = explode( '=', $string ); $_key = explode( ';', $_tmp[0] ); $tmp = explode( ':', $_tmp[1] ); } else { $_tmp = array(); $_key = array(); $tmp = explode( ':', $string ); $tmp[0] = 'DATETIME'; } $tzid = ''; //----------------------------------------- // Got a TZID? //----------------------------------------- if ( $_key[1] == 'TZID' ) { $tzid = $tmp[0]; } //----------------------------------------- // Is it a date? //----------------------------------------- if ( $tmp[0] == 'DATE' ) { $timestamp = strtotime( $tmp[1] ); } else { $timestamp = '?'; } $return = array( 'type' => $tmp[0], 'raw' => $tmp[1], 'raw_ts' => strtotime( $tmp[1] ), 'ts' => $timestamp, 'tzid' => str_replace( '"', '', $tzid ), ); //----------------------------------------- // Earliest? Latest? //----------------------------------------- if ( ( $this->_feedEarliest == 0 ) OR ( $return['raw_ts'] < $this->_feedEarliest ) ) { $this->_feedEarliest = $return['raw_ts']; } if ( ( $this->_feedLatest == 0 ) OR ( $return['raw_ts'] > $this->_feedLatest ) ) { $this->_feedLatest = $return['raw_ts']; } //----------------------------------------- // Return //----------------------------------------- return $return; } /** * Unformat content from incoming ical feed * * @param string $string String content to unparse * @param int $line Line number * @return mixed Array of data, or false */ protected function _unparseContent( $string, $line ) { //----------------------------------------- // If line starts with a space, it was folded (skip) //----------------------------------------- if( substr( $this->_rawIcsData[ $line ], 0, 1 ) == ' ' ) { return false; } //----------------------------------------- // Process //----------------------------------------- $_temp = preg_split( "/(:|;)/", $string ); $_type = array_shift( $_temp ); $_data = implode( ':', $_temp ); //----------------------------------------- // Unfold folded lines if necessary //----------------------------------------- $_data = $this->_unfoldLines( $_data, $line ); //----------------------------------------- // Return data //----------------------------------------- return array( 'type' => $_type, 'data' => $_data ); } /** * Get event recurrence data * * @param string Data * @return array */ protected function _getRecurrenceData( $data ) { $tmp = explode( ':', $data ); foreach( $tmp as $t ) { $recurr = explode( '=', $t ); $rrule_array[ $recurr[0] ] = $recurr[1]; //----------------------------------------- // Format a timestamp for the 'UNTIL' date //----------------------------------------- if ( $recurr[0] == 'UNTIL' ) { $rrule_array['until_ts'] = strtotime( $recurr[1] ); } } return $rrule_array; } /** * Retrieve stored error message * * @return string */ public function getError() { return $this->_error; } /** * Import and parse an iCalendar feed * * @param string $content Feed content * @param int $member_id Member to save events under * @param int $feed_id Feed id * @return mixed False on failure, otherwise an array with keys 'skipped' and 'imported' */ public function import( $content, $member_id=0, $feed_id=0 ) { //----------------------------------------- // Init //----------------------------------------- $this->lang->loadLanguageFile( array( 'admin_calendar' ), 'calendar' ); //----------------------------------------- // Error checking //----------------------------------------- if( !$content ) { $this->_error = $this->lang->words['icali_nocontent']; return; } $_raw = preg_replace( "#(\n\r|\r|\n){1,}#", "\n", $content ); $_raw = explode( "\n", $_raw ); if( !count($_raw) ) { $this->_error = $this->lang->words['icali_nocontent']; return; } if( $_raw[0] != 'BEGIN:VCALENDAR' ) { $this->_error = $this->lang->words['icali_badcontent']; return; } $this->_rawIcsData = $_raw; //----------------------------------------- // Loop and start parsing //----------------------------------------- foreach( $this->_rawIcsData as $k => $v ) { $line = explode( ':', $v ); switch( $line[0] ) { case 'BEGIN': $this->_parseBeginBlock( $line[1], $k ); break; /* Unsupported at this time */ case 'CALSCALE': case 'METHOD': case 'X-WR-TIMEZONE': case 'X-WR-RELCALID': default: break; } } //----------------------------------------- // Process raw ICS data now //----------------------------------------- if( count($this->_parsedIcsData) ) { $this->_parsedIcsData = $this->_convertToGmt( $this->_parsedIcsData ); } //----------------------------------------- // And loop over results to insert //----------------------------------------- $_imported = 0; $_skipped = 0; // Leave this here - useful for debugging //print_r($this->_parsedIcsData);exit; if ( count( $this->_parsedIcsData ) ) { //----------------------------------------- // Get member data //----------------------------------------- $_member = IPSMember::load( $member_id ); if( !$_member ) { $this->_error = $this->lang->words['icali_nomember']; return false; } //----------------------------------------- // Get like class for notifications //----------------------------------------- require_once( IPS_ROOT_PATH . 'sources/classes/like/composite.php' );/*noLibHook*/ $_like = classes_like::bootstrap( 'calendar', 'calendars' ); //----------------------------------------- // Loop over the events //----------------------------------------- foreach( $this->_parsedIcsData['events'] as $event ) { $event['uid'] = $event['uid'] ? $event['uid'] : md5( implode( ',', $event ) ); $event_unix_from = $event['start']['gmt_ts']; $event_unix_to = $event['end']['gmt_ts']; $event_all_day = ( $event['start']['type'] == 'DATE' ) ? 1 : 0; //----------------------------------------- // End dates are "exclusive" in iCalendar format, meaning // they are actually the day ahead. // @link http://microformats.org/wiki/dtend-issue //----------------------------------------- if( $event_unix_to AND $event['end']['type'] == 'DATE' ) { $event_unix_to -= 86400; } //----------------------------------------- // If end date is same as start date, it's a single day event //----------------------------------------- if( $event_unix_from == $event_unix_to ) { $event_unix_to = 0; } //----------------------------------------- // If no end date, but we have a duration, calculate end date //----------------------------------------- if ( ! $event_unix_to AND $event['duration'] ) { preg_match( "#(\d+?)H#is", $event['duration'], $match ); $hour = $match[1] ? $match[1] : 0; preg_match( "#(\d+?)M#is", $event['duration'], $match ); $minute = $match[1] ? $match[1] : 0; preg_match( "#(\d+?)S#is", $event['duration'], $match ); $second = $match[1] ? $match[1] : 0; $event_unix_to = $event_unix_from + ( $hour * 3600 ) + ( $minute * 60 ) + $second; } //----------------------------------------- // Recurring... //----------------------------------------- $recurring = 0; if ( isset( $event['recurr']['FREQ'] ) ) { if ( $event['recurr']['FREQ'] == 'MONTHLY' AND $event['recurr']['INTERVAL'] == 12 ) { $event['recurr']['FREQ'] = 'YEARLY'; } switch( $event['recurr']['FREQ'] ) { case 'WEEKLY': $recurring = 1; break; case 'MONTHLY': $recurring = 2; break; case 'YEARLY': $recurring = 3; break; } $event_unix_to = ( $event['recurr']['until_ts'] ) ? $event['recurr']['until_ts'] : time() + 86400 * 365 * 10; } //----------------------------------------- // Adjust timestamps if all day event //----------------------------------------- if( $event_all_day ) { $event_unix_from = gmmktime( 0, 0, 0, gmstrftime( '%m', $event_unix_from ), gmstrftime( '%d', $event_unix_from ), gmstrftime( '%Y', $event_unix_from ) ); $event_unix_to = $event_unix_to ? gmmktime( 0, 0, 0, gmstrftime( '%m', $event_unix_to ), gmstrftime( '%d', $event_unix_to ), gmstrftime( '%Y', $event_unix_to ) ) : 0; } //----------------------------------------- // If we are missing crucial data, skip //----------------------------------------- if( !($event['description'] OR $event['summary']) OR !$event_unix_from ) { $_skipped++; continue; } //----------------------------------------- // Skip previously imported events //----------------------------------------- if( $event['uid'] ) { $_check = $this->DB->buildAndFetch( array( 'select' => 'import_id', 'from' => 'cal_import_map', 'where' => "import_guid='" . $this->DB->addSlashes( $event['uid'] ) . "'" ) ); if( $_check['import_id'] ) { $_skipped++; continue; } } //----------------------------------------- // Format array for storage //----------------------------------------- $_eventData = array( 'event_calendar_id' => $this->calendar['cal_id'], 'event_member_id' => $_member['member_id'], 'event_content' => $event['description'] ? nl2br($event['description']) : $event['summary'], 'event_title' => $event['summary'] ? $event['summary'] : IPSText::truncate( $event['description'], 100 ), 'event_title_seo' => IPSText::makeSeoTitle( $event['summary'] ), 'event_smilies' => 1, 'event_comments' => 0, 'event_rsvp' => count($event['attendee']) ? 1 : 0, 'event_perms' => '*', 'event_private' => 0, 'event_approved' => 1, 'event_saved' => $event['created'] ? $event['created'] : time(), 'event_lastupdated' => $event['last_modified'] ? $event['last_modified'] : ( $event['created'] ? $event['created'] : time() ), 'event_recurring' => $recurring, 'event_start_date' => strftime( "%Y-%m-%d %H:%M:00", $event_unix_from ), 'event_end_date' => $event_unix_to ? strftime( "%Y-%m-%d %H:%M:00", $event_unix_to ) : 0, 'event_post_key' => md5( uniqid( microtime(), true ) ), 'event_sequence' => intval($event['sequence']), 'event_all_day' => $event_all_day, ); //----------------------------------------- // Data hooks //----------------------------------------- IPSLib::doDataHooks( $_eventData, 'calendarAddEvent' ); //----------------------------------------- // Insert //----------------------------------------- $this->DB->insert( 'cal_events', $_eventData ); $event_id = $this->DB->getInsertId(); $_imported++; //----------------------------------------- // Insert mapping //----------------------------------------- $this->DB->insert( 'cal_import_map', array( 'import_feed_id' => $feed_id, 'import_event_id' => $event_id, 'import_guid' => $event['uid'], ) ); //----------------------------------------- // If we have attendees that are members, insert them //----------------------------------------- if( isset($event['attendee']) AND count($event['attendee']) ) { foreach( $event['attendee'] as $attendee ) { if( $attendee['email'] ) { $_loadedMember = IPSMember::load( $attendee['email'] ); if( $_loadedMember['member_id'] ) { $this->DB->insert( 'cal_event_rsvp', array( 'rsvp_member_id' => $_loadedMember['member_id'], 'rsvp_event_id' => $event_id, 'rsvp_date' => time(), ) ); } } } } //----------------------------------------- // Send notifications //----------------------------------------- $_url = $this->registry->output->buildSEOUrl( 'app=calendar&module=calendar&section=view&do=showevent&event_id=' . $event_id, 'public', $_eventData['event_title_seo'], 'cal_event' ); $_like->sendNotifications( $_eventData['event_calendar_id'], array( 'immediate', 'offline' ), array( 'notification_key' => 'new_event', 'notification_url' => $_url, 'email_template' => 'add_event_follow', 'email_subject' => sprintf( $this->lang->words[ 'add_event_follow_subject'], $_url, $_eventData['event_title'] ), 'build_message_array' => array( 'NAME' => '-member:members_display_name-', 'AUTHOR' => $_member['members_display_name'], 'TITLE' => $_eventData['event_title'], 'URL' => $_url, ) ) ); } } //----------------------------------------- // Rebuild cache //----------------------------------------- $this->cache->rebuildCache( 'calendar_events', 'calendar' ); //----------------------------------------- // Return //----------------------------------------- return array( 'skipped' => $_skipped, 'imported' => $_imported ); } /** * Convert times to GMT based on timezones * * @param array $data Parsed data * @return array */ protected function _convertToGmt( $data ) { //----------------------------------------- // INIT //----------------------------------------- $timezones = array(); //----------------------------------------- // Got any timezones? //----------------------------------------- if ( is_array( $data['timezones'] ) AND count( $data['timezones'] ) ) { foreach( $data['timezones'] as $type => $_data ) { $timezones[ $type ] = $_data; } } //----------------------------------------- // Fix up events //----------------------------------------- if ( is_array( $data['events'] ) AND count( $data['events'] ) ) { foreach( $data['events'] as $id => $event ) { foreach( array( 'start', 'end' ) as $method ) { //----------------------------------------- // Set up constraints //----------------------------------------- if ( isset( $event[ $method ]['tzid'] ) AND isset( $timezones[ $event[ $method ]['tzid'] ] ) AND is_array( $timezones[ $event[ $method ]['tzid'] ] ) ) { $_standard = ( isset($timezones[ $event[ $method ]['tzid'] ]['standard']['start'] ) ) ? intval( $timezones[ $event[ $method ]['tzid'] ]['standard']['start'] ) : 0; $_daylight = ( isset($timezones[ $event[ $method ]['tzid'] ]['daylight']['start'] ) ) ? intval( $timezones[ $event[ $method ]['tzid'] ]['daylight']['start'] ) : 0; $_offset = 0; if ( isset( $event[ $method ]['tzid'] ) AND $event[ $method ]['raw_ts'] ) { //if ( $event[ $method ]['raw_ts'] < $_daylight ) //{ // $_offset = $timezones[ $event[ $method ]['tzid'] ]['standard']['tz_offset_to']; //} //else if ( $event[ $method ]['raw_ts'] > $_standard ) //{ // $_offset = $timezones[ $event[ $method ]['tzid'] ]['standard']['tz_offset_to']; //} //else //{ // $_offset = $timezones[ $event[ $method ]['tzid'] ]['daylight']['tz_offset_to']; //} $_offset = $timezones[ $event[ $method ]['tzid'] ]['standard']['tz_offset_to']; $event[ $method ]['gmt_ts'] = $event[ $method ]['raw_ts'] - ( $_offset / 100 * 3600 ); $event[ $method ]['offset'] = $_offset; $event[ $method ]['gmt_rfc'] = gmdate( 'r', $event[ $method ]['gmt_ts'] ); } } else { if ( isset( $event[ $method ]['raw_ts'] ) ) { $event[ $method ]['gmt_ts'] = $event[ $method ]['raw_ts']; $event[ $method ]['offset'] = 0; $event[ $method ]['gmt_rfc'] = gmdate( 'r', $event[ $method ]['gmt_ts'] ); } } } $data['events'][ $id ] = $event; } } return $data; } /** * Parse a 'BEGIN:' block in an iCalendar feed * * @param string $type Type of 'BEGIN' object * @param int $start Line number * @return @e void */ protected function _parseBeginBlock( $type, $start ) { switch( $type ) { case 'VCALENDAR': $this->_begin = 'VCALENDAR'; $this->_processVcalendarObject( $start + 1 ); break; case 'VTIMEZONE': $this->_begin = 'VTIMEZONE'; $this->_processTimezoneObject( $start + 1 ); break; case 'STANDARD': if ( $this->_begin == 'VTIMEZONE' ) { $this->_processTimezoneTypeObject( $start + 1, 'STANDARD' ); } break; case 'DAYLIGHT': if ( $this->_begin == 'VTIMEZONE' ) { $this->_processTimezoneTypeObject( $start + 1, 'DAYLIGHT' ); } break; case 'VEVENT': $this->_begin = 'VEVENT'; $this->_processEventObject( $start + 1 ); break; /* Anything else is unsupported at this time */ default: break; } } /** * Parse event object in an ical feed * * @param int $start Line number * @return @e void * @link http://community.invisionpower.com/resources/bugs.html/_/ip-calendar/recurring-events-can-sometimes-be-skipped-in-ics-r41033 */ protected function _processEventObject( $start ) { //----------------------------------------- // Init //----------------------------------------- $_break = false; $_event = array(); //----------------------------------------- // Loop over lines //----------------------------------------- $_recid = null; for( $i = $start, $j = count( $this->_rawIcsData ); $i < $j; $i++ ) { //----------------------------------------- // Unparse and get the data //----------------------------------------- $tmp = $this->_unparseContent( $this->_rawIcsData[$i], $i ); if ( !$tmp ) { continue; } $_type = $tmp['type']; $_data = $tmp['data']; switch( $_type ) { case 'CLASS': $_event['access_class'] = $_data; break; case 'CREATED': if( !$_event['created'] ) { $_event['created'] = strtotime( $_data ); } break; case 'SUMMARY': /* @link http://community.invisionpower.com/tracker/issue-32941-ical-summary/ */ if( strpos( $_data, 'LANGUAGE=' ) === 0 ) { $_data = preg_replace( "/^LANGUAGE=(.+?):(.+?)$/i", "\\2", $_data ); } $_event['summary'] = $this->_unencodeSpecialCharacters( $_data ); break; case 'DESCRIPTION': $_event['description'] = $this->_unencodeSpecialCharacters( $_data ); break; case 'DURATION': $_event['duration'] = $_data; break; case 'DTSTART': $_event['start'] = $this->_unparseTimeInfo( $this->_rawIcsData[$i] ); break; case 'DTEND': $_event['end'] = $this->_unparseTimeInfo( $this->_rawIcsData[$i] ); break; case 'DTSTAMP': $_event['created'] = strtotime( $_data ); break; case 'LAST-MODIFIED': $_event['last_modified'] = strtotime( $_data ); break; case 'TRANSP': $_event['time_transparent'] = $_data; break; case 'GEO': $_event['geo'] = $_data; break; case 'ORGANIZER': $line = explode( ':', $_data ); $_event['organizer'] = array( 'name' => str_replace( 'CN=', '', $line[0] ), 'email' => $line[2] ); break; case 'ATTENDEE': $line = explode( ':', $_data ); $_email = ''; foreach( $line as $_line ) { $_line = str_replace( 'cn=', '', strtolower($_line) ); if( IPSText::checkEmailAddress( $_line ) ) { $_email = $_line; } } $_event['attendee'][] = array( 'name' => str_replace( 'CN=', '', $line[0] ), 'email' => $_email ); break; case 'UID': $_event['uid'] = $_data; break; case 'STATUS': $_event['status'] = $_data; break; case 'LOCATION': $_event['location'] = $_data; break; case 'SEQUENCE': $_event['sequence'] = intval($_data); break; case 'RRULE': $_event['recurr'] = $_event['recurr'] ? $_event['recurr'] : array(); $_event['recurr'] = array_merge( $_event['recurr'], $this->_getRecurrenceData( $_data ) ); break; case 'BEGIN': $this->_parseBeginBlock( $_data, $i ); break; case 'RECURRENCE-ID': $_recid = $_data; break; case 'END': $_break = true; break; } if( $_break ) { if( $_recid ) { $event['uid'] = md5( $event['uid'] . $_recid ); } $this->_parsedIcsData['events'][] = $_event; break; } } } /** * Parse core vcalendar object in an ical feed * * @param int $start Line number * @return @e void */ protected function _processVcalendarObject( $start ) { //----------------------------------------- // Loop over lines //----------------------------------------- for( $i = $start, $j = count( $this->_rawIcsData ); $i < $j; $i++ ) { //----------------------------------------- // Unparse and get the data //----------------------------------------- $tmp = $this->_unparseContent( $this->_rawIcsData[$i], $i ); if ( !$tmp ) { continue; } $_type = $tmp['type']; $_data = $tmp['data']; switch( $_type ) { case 'PRODID': $this->_parsedIcsData['core']['product'] = $_data; break; case 'VERSION': $this->_parsedIcsData['core']['version'] = $_data; break; case 'BEGIN': $this->_parseBeginBlock( $_data, $i ); break; case 'X-WR-CALNAME': $this->_parsedIcsData['core']['calendar_name'] = $_data; break; case 'END': return; break; } } } /** * Parse a timezone object in an ical feed * * @param int $start Line number * @param string $type Type of time zone object to parse * @return @e void */ protected function _processTimezoneTypeObject( $start, $type ) { //----------------------------------------- // INIT //----------------------------------------- $type = strtolower( $type ); $break = FALSE; //----------------------------------------- // Loop over line numbers //----------------------------------------- for( $i = $start, $j = count( $this->_rawIcsData ); $i < $j; $i++ ) { //----------------------------------------- // Unparse and get the data //----------------------------------------- $tmp = $this->_unparseContent( $this->_rawIcsData[$i], $i ); if ( !$tmp ) { continue; } $_type = $tmp['type']; $_data = $tmp['data']; switch( $_type ) { case 'DTSTART': $this->_parsedIcsData['timezones'][ $this->_tzId ][ $type ]['start'] = strtotime( $_data ); break; case 'TZOFFSETTO': $this->_parsedIcsData['timezones'][ $this->_tzId ][ $type ]['tz_offset_to'] = $_data; break; case 'TZOFFSETFROM': $this->_parsedIcsData['timezones'][ $this->_tzId ][ $type ]['tz_offset_from'] = $_data; break; case 'TZNAME': $this->_parsedIcsData['timezones'][ $this->_tzId ][ $type ]['tz_name'] = $_data; break; case 'END': return; break; } } } /** * Parse a timezone object in an ical feed * * @param int $start Line number * @return @e void */ protected function _processTimezoneObject( $start ) { //----------------------------------------- // Loop over lines //----------------------------------------- for( $i = $start, $j = count( $this->_rawIcsData ); $i < $j; $i++ ) { //----------------------------------------- // Unparse and get the data //----------------------------------------- $tmp = $this->_unparseContent( $this->_rawIcsData[$i], $i ); if( !$tmp ) { continue; } $_type = $tmp['type']; $_data = $tmp['data']; switch( $_type ) { case 'TZID': $this->_tzId = $_data; $this->_parsedIcsData['timezones'][ $_data ] = array(); break; case 'LAST-MODIFIED': $this->_parsedIcsData['timezones'][ $this->_tzId ]['last_modified'] = $_data; break; case 'BEGIN': $this->_tzBegin = $_data; $this->_parseBeginBlock( $_data, $i ); break; case 'END': if ( $this->_tzBegin == $_data ) { return; } break; } } } }