2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
40 require_once('include/utils.php');
41 require_once('modules/Calendar/Calendar.php');
42 require_once('modules/vCals/vCal.php');
45 * Class for constructing the iCal response string for the current user.
49 class iCal extends vCal {
51 const UTC_FORMAT = 'Ymd\THi00\Z';
54 * Constructor for the iCal class.
56 public function __construct()
62 * Escapes new lines in the given string,
64 * @param string $string the original string
65 * @return string the string with new lines properly escaped
67 protected function escapeNls($string)
69 $str = str_replace(array("\r\n", "\n"), "\\n", $string);
74 * Gets a UTC formatted string from the given dateTime
76 * @param SugarDateTime $dateTime the dateTime to format
77 * @return string the UTC formatted dateTime
79 protected function getUtcDateTime($dateTime)
81 return $dateTime->format(self::UTC_FORMAT);
85 * Gets the UTC formatted dateTime from the given timestamp.
87 * Checks the version of Sugar to see if user timezone adjustments are needed.
89 * @param integer $ts the timestamp to format
90 * @return string the UTC formatted dateTime
92 protected function getUtcTime($ts)
94 global $timedate, $sugar_version;
95 $timestamp = ($ts+(date('Z')-$timedate->adjustmentForUserTimeZone()*60));
96 return $this->getUtcDateTime(new SugarDateTime("@" . $ts));
100 * Converts the given number of minutes to formatted number of hours and remaining minutes.
102 * @param integer $minutes the number of minutes to format
103 * @return string the formatted hours and minutes
105 protected function convertMinsToHoursAndMins($minutes)
107 $hrs = floor(abs($minutes) / 60);
108 $remainderMinutes = abs($minutes) - ($hrs * 60);
109 $sign = (($minutes < 0) ? "-" : "+");
110 return $sign . str_pad($hrs, 2, "0", STR_PAD_LEFT) . str_pad($remainderMinutes, 2, "0", STR_PAD_LEFT);
114 * Create a todo entry for the given task.
116 * @param UserBean $user_bean the current UserBean
117 * @param Task $task the task for the todo entry
118 * @param string $moduleName the name of the task module
119 * @param string $dtstamp the current timestamp
120 * @return string the todo entry for the task
122 protected function createSugarIcalTodo($user_bean, $task, $moduleName, $dtstamp)
124 global $sugar_config;
126 $str .= "BEGIN:VTODO\n";
127 $validDueDate = (isset($task->date_due) && $task->date_due != "" && $task->date_due != "0000-00-00");
128 $validDueTime = (isset($task->time_due) && $task->time_due != "");
135 $dateDueArr = split("-", $task->date_due);
136 $dueYear = (int)$dateDueArr[0];
137 $dueMonth = (int)$dateDueArr[1];
138 $dueDay = (int)$dateDueArr[2];
141 $timeDueArr = split(":", $task->time_due);
142 $dueHour = (int)$timeDueArr[0];
143 $dueMin = (int)$timeDueArr[1];
152 $due_date_time = new SugarDateTime();
153 $due_date_time->setDate($dueYear, $dueMonth, $dueDay);
154 $due_date_time->setTime($dueHour, $dueMin);
155 $str .= "DTSTART;TZID=" . $user_bean->getPreference('timezone') . ":" .
156 str_replace("Z", "", $this->getUtcDateTime($due_date_time)) . "\n";
157 $str .= "DTSTAMP:" . $dtstamp . "\n";
158 $str .= "SUMMARY:" . $task->name . "\n";
159 $str .= "UID:" . $task->id . "\n";
161 $iCalDueDate = str_replace("-", "", $task->date_due);
162 if (strlen($iCalDueDate) > 8) {
163 $iCalDueDate = substr($iCalDueDate, 0, 8);
165 $str .= "DUE;VALUE=DATE:" . $iCalDueDate . "\n";
167 if ($moduleName == "ProjectTask") {
168 $str .= "DESCRIPTION:Project: " . $task->project_name. "\\n\\n" .
169 $this->escapeNls($task->description). "\n";
171 $str .= "DESCRIPTION:" . $this->escapeNls($task->description). "\n";
173 $str .= "URL;VALUE=URI:" . $sugar_config['site_url'].
174 "/index.php?module=".$moduleName."&action=DetailView&record=". $task->id. "\n";
175 if ($task->status == 'Completed') {
176 $str .= "STATUS:COMPLETED\n";
177 $str .= "PERCENT-COMPLETE:100\n";
178 $str .= "COMPLETED:" . $this->getUtcDateTime($due_date_time) . "\n";
179 } else if (!empty($task->percent_complete)) {
180 $str .= "PERCENT-COMPLETE:" . $task->percent_complete . "\n";
182 if ($task->priority == "Low") {
183 $str .= "PRIORITY:9\n";
184 } else if ($task->priority == "Medium") {
185 $str .= "PRIORITY:5\n";
186 } else if ($task->priority == "High") {
187 $str .= "PRIORITY:1\n";
189 $str .= "END:VTODO\n";
194 * Creates the string for the user's events and todos between the given start
197 * @param UserBean $user_bean the current UserBean
198 * @param DateTime $start_date_time the start date to search from
199 * @param DateTime $end_date_time the end date to search to
200 * @param string $dtstamp the current timestamp
201 * @return string the entries for events and todos
203 protected function createSugarIcal(&$user_bean,&$start_date_time,&$end_date_time, $dtstamp)
206 global $DO_USER_TIME_OFFSET, $sugar_config, $current_user, $timedate;
209 if (!empty($_REQUEST['hide_calls']) && $_REQUEST['hide_calls'] == "true")
215 if (!empty($_REQUEST['show_tasks_as_events']) && ($_REQUEST['show_tasks_as_events'] == "1" || $_REQUEST['show_tasks_as_events'] == "true"))
217 $taskAsVTODO = false;
220 $acts_arr = CalendarActivity::get_activities($user_bean->id,
228 // loop thru each activity, get start/end time in UTC, and return iCal strings
229 foreach($acts_arr as $act)
232 $event = $act->sugar_bean;
233 if (!$hide_calls || ($hide_calls && $event->object_name != "Call"))
235 $str .= "BEGIN:VEVENT\n";
236 $str .= "SUMMARY:" . $event->name . "\n";
237 $str .= "DTSTART;TZID=" . $user_bean->getPreference('timezone') . ":" .
238 str_replace("Z", "", $timedate->tzUser($act->start_time, $current_user)->format(self::UTC_FORMAT)) . "\n";
239 $str .= "DTEND;TZID=" . $user_bean->getPreference('timezone') . ":" .
240 str_replace("Z", "", $timedate->tzUser($act->end_time, $current_user)->format(self::UTC_FORMAT)) . "\n";
241 $str .= "DTSTAMP:" . $dtstamp . "\n";
242 $str .= "DESCRIPTION:" . $this->escapeNls($event->description) . "\n";
243 $str .= "URL;VALUE=URI:" . $sugar_config['site_url'].
244 "/index.php?module=".$event->module_dir."&action=DetailView&record=". $event->id. "\n";
245 $str .= "UID:" . $event->id . "\n";
246 if ($event->object_name == "Meeting")
248 $str .= "LOCATION:" . $event->location . "\n";
249 $eventUsers = $event->get_meeting_users();
250 $query = "SELECT contact_id as id from meetings_contacts where meeting_id='$event->id' AND deleted=0";
251 $eventContacts = $event->build_related_list($query, new Contact());
252 $eventAttendees = array_merge($eventUsers, $eventContacts);
253 if (is_array($eventAttendees))
255 foreach($eventAttendees as $attendee)
257 if ($attendee->id != $user_bean->id)
259 // Define the participant status
260 $participant_status = '';
261 if (!empty($attendee->accept_status)) {
262 switch ($attendee->accept_status) {
264 $participant_status = ';PARTSTAT=ACCEPTED';
267 $participant_status = ';PARTSTAT=DECLINED';
270 $participant_status = ';PARTSTAT=TENTATIVE';
274 $str .= 'ATTENDEE'.$participant_status.';CN="'.$attendee->get_summary_text().'":mailto:'. (!empty($attendee->email1) ? $attendee->email1 : 'none@none.tld') . "\n";
279 if ($event->object_name == "Call")
281 $eventUsers = $event->get_call_users();
282 $eventContacts = $event->get_contacts();
283 $eventAttendees = array_merge($eventUsers, $eventContacts);
284 if (is_array($eventAttendees))
286 foreach($eventAttendees as $attendee)
288 if ($attendee->id != $user_bean->id)
290 // Define the participant status
291 $participant_status = '';
292 if (!empty($attendee->accept_status)) {
293 switch ($attendee->accept_status) {
295 $participant_status = ';PARTSTAT=ACCEPTED';
298 $participant_status = ';PARTSTAT=DECLINED';
301 $participant_status = ';PARTSTAT=TENTATIVE';
305 $str .= 'ATTENDEE'.$participant_status.';CN="'.$attendee->get_summary_text().'":mailto:'. (!empty($attendee->email1) ? $attendee->email1 : 'none@none.tld') . "\n";
310 if (isset($event->reminder_time) && $event->reminder_time > 0 && $event->status != "Held")
312 $str .= "BEGIN:VALARM\n";
313 $str .= "TRIGGER:-PT" . $event->reminder_time/60 . "M\n";
314 $str .= "ACTION:DISPLAY\n";
315 $str .= "DESCRIPTION:" . $event->name . "\n";
316 $str .= "END:VALARM\n";
318 $str .= "END:VEVENT\n";
323 require_once('include/TimeDate.php');
324 $timedate = new TimeDate();
325 $today = gmdate("Y-m-d");
326 $today = $timedate->handle_offset($today, $timedate->dbDayFormat, false);
328 require_once('modules/ProjectTask/ProjectTask.php');
329 $where = "project_task.assigned_user_id='{$user_bean->id}' ".
330 "AND (project_task.status IS NULL OR (project_task.status!='Deferred')) ".
331 "AND (project_task.date_start IS NULL OR " . CalendarActivity::get_occurs_within_where_clause('project_task', '', $start_date_time, $end_date_time, 'date_start', 'month') . ")";
332 $seedProjectTask = new ProjectTask();
333 $projectTaskList = $seedProjectTask->get_full_list("", $where);
334 if (is_array($projectTaskList))
336 foreach($projectTaskList as $projectTask)
338 $str .= $this->createSugarIcalTodo($user_bean, $projectTask, "ProjectTask", $dtstamp);
343 require_once('modules/Tasks/Task.php');
344 $where = "tasks.assigned_user_id='{$user_bean->id}' ".
345 "AND (tasks.status IS NULL OR (tasks.status!='Deferred')) ".
346 "AND (tasks.date_start IS NULL OR " . CalendarActivity::get_occurs_within_where_clause('tasks', '', $start_date_time, $end_date_time, 'date_start', 'month') . ")";
347 $seedTask = new Task();
348 $taskList = $seedTask->get_full_list("", $where);
349 if (is_array($taskList))
351 foreach($taskList as $task)
353 $str .= $this->createSugarIcalTodo($user_bean, $task, "Tasks", $dtstamp);
362 * Gets the time zone for the given user.
364 * @param User $current_user the user
365 * @return DateTimeZone the user's timezone
367 protected function getUserTimezone($current_user)
369 $gmtTZ = new DateTimeZone("UTC");
370 $userTZName = TimeDate::userTimezone($current_user);
371 if (!empty($userTZName))
373 $tz = new DateTimeZone($userTZName);
382 * Gets the daylight savings range for the given user.
384 * @param User $current_user the user
385 * @param integer $year the year
386 * @return array the start and end transitions of daylight savings
388 protected function getDSTRange($current_user, $year)
390 $tz = $this->getUserTimezone($current_user);
394 if (version_compare(PHP_VERSION, '5.3.0') >= 0)
396 $year_date = SugarDateTime::createFromFormat("Y", $year, new DateTimeZone("UTC"));
397 $year_end = clone $year_date;
398 $year_end->setDate((int) $year, 12, 31);
399 $year_end->setTime(23, 59, 59);
400 $year_date->setDate((int) $year, 1, 1);
401 $year_date->setTime(0, 0, 0);
403 $transitions = $tz->getTransitions($year_date->getTimestamp(), $year_end->getTimestamp());
404 foreach($transitions as $transition) {
405 if($transition['isdst']) {
411 $transitions = $tz->getTransitions();
414 foreach($transitions as $transition) {
415 if($transition['isdst'] && intval(substr($transition["time"], 0, 4)) == intval(date("Y"))) {
422 if (empty($transitions[$idx]["isdst"])) {
423 // No DST transitions found
426 $result["start"] = $transitions[$idx]; // DST begins here
427 // scan till DST ends
428 while (isset($transitions[$idx]) && $transitions[$idx]["isdst"]) $idx++;
429 if(isset($transitions[$idx])) {
430 $result["end"] = $transitions[$idx];
436 * Gets the timezone string for the current user.
438 * @return string the full timezone definition including daylight savings for the iCal
440 protected function getTimezoneString()
442 global $current_user, $timedate;
443 $timezoneName = $current_user->getPreference('timezone');
445 $gmtTZ = new DateTimeZone("UTC");
446 $tz = $this->getUserTimezone($current_user);
447 $dstRange = $this->getDSTRange($current_user, date('Y'));
452 $timezoneString = "BEGIN:VTIMEZONE\n";
453 $timezoneString .= "TZID:" . $timezoneName . "\n";
454 $timezoneString .= "X-LIC-LOCATION:" . $timezoneName . "\n";
456 if (array_key_exists('start', $dstRange))
458 $dstOffset = ($dstRange['start']['offset'] / 60);
459 $startDate = new DateTime("@" . $dstRange["start"]["ts"], $gmtTZ);
460 $startstamp = strtotime($timedate->asDb($startDate));
461 $timezoneString .= "BEGIN:DAYLIGHT\n";
462 $timezoneString .= "TZOFFSETFROM:" . $this->convertMinsToHoursAndMins($gmtOffset) . "\n";
463 $timezoneString .= "TZOFFSETTO:" . $this->convertMinsToHoursAndMins($dstOffset) . "\n";
464 $timezoneString .= "DTSTART:" . str_replace("Z", "", $this->getUtcTime($startstamp)) . "\n";
465 $timezoneString .= "END:DAYLIGHT\n";
468 if (array_key_exists('end', $dstRange))
470 $gmtOffset = ($dstRange['end']['offset'] / 60);
471 $endDate = new DateTime("@" . $dstRange["end"]["ts"], $gmtTZ);
472 $endstamp = strtotime($timedate->asDb($endDate));
473 $timezoneString .= "BEGIN:STANDARD\n";
474 $timezoneString .= "TZOFFSETFROM:" . $this->convertMinsToHoursAndMins($dstOffset) . "\n";
475 $timezoneString .= "TZOFFSETTO:" . $this->convertMinsToHoursAndMins($gmtOffset) . "\n";
476 $timezoneString .= "DTSTART:" . str_replace("Z", "", $this->getUtcTime($endstamp)) . "\n";
477 $timezoneString .= "END:STANDARD\n";
480 $timezoneString .= "END:VTIMEZONE\n";
482 return $timezoneString;
486 * Generates the complete string for the calendar
488 * @param User $user_focus the user
489 * @param integer $num_months the number of months to search before and after today
490 * @return string the iCal calenar string
492 function getVcalIcal(&$user_focus, $num_months)
494 global $current_user, $timedate;
495 $current_user = $user_focus;
497 $cal_name = $user_focus->first_name. " ". $user_focus->last_name;
499 $str = "BEGIN:VCALENDAR\n";
500 $str .= "VERSION:2.0\n";
501 $str .= "METHOD:PUBLISH\n";
502 $str .= "X-WR-CALNAME:$cal_name (SugarCRM)\n";
503 $str .= "PRODID:-//SugarCRM//SugarCRM Calendar//EN\n";
504 $str .= $this->getTimezoneString();
505 $str .= "CALSCALE:GREGORIAN\n";
507 $now_date_time = $timedate->getNow(true);
509 global $sugar_config;
511 if (isset($sugar_config['vcal_time']) && $sugar_config['vcal_time'] != 0 && $sugar_config['vcal_time'] < 13)
513 $timeOffset = $sugar_config['vcal_time'];
515 if (!empty($num_months))
517 $timeOffset = $num_months;
519 $start_date_time = $now_date_time->get("-$timeOffset months");
520 $end_date_time = $now_date_time->get("+$timeOffset months");
522 $utc_now_time = $this->getUtcDateTime($now_date_time);
524 $str .= $this->createSugarIcal($user_focus,$start_date_time,$end_date_time,$utc_now_time);
526 $str .= "DTSTAMP:" . $utc_now_time . "\n";
527 $str .= "END:VCALENDAR\n";