]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - modules/iCals/iCal.php
Release 6.5.10
[Github/sugarcrm.git] / modules / iCals / iCal.php
1 <?php
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.
6  * 
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.
13  * 
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
17  * details.
18  * 
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
22  * 02110-1301 USA.
23  * 
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.
26  * 
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.
30  * 
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  ********************************************************************************/
37
38
39
40 require_once('include/utils.php');
41 require_once('modules/Calendar/Calendar.php');
42 require_once('modules/vCals/vCal.php');
43
44 /**
45 * Class for constructing the iCal response string for the current user.
46 *
47 * @see vCal
48 */
49 class iCal extends vCal {
50
51     const UTC_FORMAT = 'Ymd\THi00\Z';
52
53     /**
54     * Constructor for the iCal class.
55     */
56     public function __construct()
57     {
58         parent::vCal();
59     }
60
61     /**
62     * Escapes new lines in the given string,
63     *
64     * @param string $string the original string
65     * @return string the string with new lines properly escaped
66     */
67     protected function escapeNls($string)
68     {
69         $str = str_replace(array("\r\n", "\n"), "\\n", $string);
70         return $str;
71     }
72
73     /**
74     * Gets a UTC formatted string from the given dateTime
75     *
76     * @param SugarDateTime $dateTime the dateTime to format
77     * @return string the UTC formatted dateTime
78     */
79     protected function getUtcDateTime($dateTime)
80     {
81         return $dateTime->format(self::UTC_FORMAT);
82     }
83
84     /**
85     * Gets the UTC formatted dateTime from the given timestamp.
86     *
87     * Checks the version of Sugar to see if user timezone adjustments are needed.
88     *
89     * @param integer $ts the timestamp to format
90     * @return string the UTC formatted dateTime
91     */
92     protected function getUtcTime($ts)
93     {
94         global $timedate, $sugar_version;
95         $timestamp = ($ts+(date('Z')-$timedate->adjustmentForUserTimeZone()*60));
96         return $this->getUtcDateTime(new SugarDateTime("@" . $ts));
97     }
98
99     /**
100     * Converts the given number of minutes to formatted number of hours and remaining minutes.
101     *
102     * @param integer $minutes the number of minutes to format
103     * @return string the formatted hours and minutes
104     */
105     protected function convertMinsToHoursAndMins($minutes)
106     {
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);
111     }
112
113     /**
114     * Create a todo entry for the given task.
115     *
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
121     */
122     protected function createSugarIcalTodo($user_bean, $task, $moduleName, $dtstamp)
123     {
124         global $sugar_config;
125         $str = "";
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 != "");
129         $dueYear = 1970;
130         $dueMonth = 1;
131         $dueDay = 1;
132         $dueHour = 0;
133         $dueMin = 0;
134         if ($validDueDate) {
135             $dateDueArr = split("-", $task->date_due);
136             $dueYear = (int)$dateDueArr[0];
137             $dueMonth = (int)$dateDueArr[1];
138             $dueDay = (int)$dateDueArr[2];
139
140             if ($validDueTime) {
141                 $timeDueArr = split(":", $task->time_due);
142                 $dueHour = (int)$timeDueArr[0];
143                 $dueMin = (int)$timeDueArr[1];
144             }
145         }
146         $date_arr = array(
147              'day'=>$dueDay,
148              'month'=>$dueMonth,
149              'hour'=>$dueHour,
150              'min'=>$dueMin,
151              'year'=>$dueYear);
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";
160         if ($validDueDate) {
161             $iCalDueDate = str_replace("-", "", $task->date_due);
162             if (strlen($iCalDueDate) > 8) {
163                 $iCalDueDate = substr($iCalDueDate, 0, 8);
164             }
165             $str .= "DUE;VALUE=DATE:" . $iCalDueDate . "\n";
166         }
167         if ($moduleName == "ProjectTask") {
168             $str .= "DESCRIPTION:Project: " . $task->project_name. "\\n\\n" .
169                 $this->escapeNls($task->description). "\n";
170         } else {
171             $str .= "DESCRIPTION:" . $this->escapeNls($task->description). "\n";
172         }
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";
181         }
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";
188         }
189         $str .= "END:VTODO\n";
190         return $str;
191     }
192
193     /**
194     * Creates the string for the user's events and todos between the given start
195     * and end times
196     *
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
202     */
203     protected function createSugarIcal(&$user_bean,&$start_date_time,&$end_date_time, $dtstamp)
204     {
205         $str = '';
206         global $DO_USER_TIME_OFFSET, $sugar_config, $current_user, $timedate;
207
208         $hide_calls = false;
209         if (!empty($_REQUEST['hide_calls']) && $_REQUEST['hide_calls'] == "true")
210         {
211             $hide_calls = true;
212         }
213
214         $taskAsVTODO = true;
215         if (!empty($_REQUEST['show_tasks_as_events']) && ($_REQUEST['show_tasks_as_events'] == "1"  || $_REQUEST['show_tasks_as_events'] == "true"))
216         {
217             $taskAsVTODO = false;
218         }
219
220         $acts_arr = CalendarActivity::get_activities($user_bean->id,
221             !$taskAsVTODO,
222             $start_date_time,
223             $end_date_time,
224             'month',
225             !$hide_calls);
226
227
228         // loop thru each activity, get start/end time in UTC, and return iCal strings
229         foreach($acts_arr as $act)
230         {
231
232             $event = $act->sugar_bean;
233             if (!$hide_calls || ($hide_calls && $event->object_name != "Call"))
234             {
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")
247                 {
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))
254                     {
255                         foreach($eventAttendees as $attendee)
256                         {
257                             if ($attendee->id != $user_bean->id)
258                             {
259                                 // Define the participant status
260                                 $participant_status = '';
261                                 if (!empty($attendee->accept_status)) {
262                                     switch ($attendee->accept_status) {
263                                         case 'accept':
264                                             $participant_status = ';PARTSTAT=ACCEPTED';
265                                             break;
266                                         case 'decline':
267                                             $participant_status = ';PARTSTAT=DECLINED';
268                                             break;
269                                         case 'tentative':
270                                             $participant_status = ';PARTSTAT=TENTATIVE';
271                                             break;
272                                     }
273                                 }
274                                 $str .= 'ATTENDEE'.$participant_status.';CN="'.$attendee->get_summary_text().'":mailto:'. (!empty($attendee->email1) ? $attendee->email1 : 'none@none.tld') . "\n";
275                             }
276                         }
277                     }
278                 }
279                 if ($event->object_name == "Call")
280                 {
281                     $eventUsers = $event->get_call_users();
282                     $eventContacts = $event->get_contacts();
283                     $eventAttendees = array_merge($eventUsers, $eventContacts);
284                     if (is_array($eventAttendees))
285                     {
286                         foreach($eventAttendees as $attendee)
287                         {
288                             if ($attendee->id != $user_bean->id)
289                             {
290                                 // Define the participant status
291                                 $participant_status = '';
292                                 if (!empty($attendee->accept_status)) {
293                                     switch ($attendee->accept_status) {
294                                         case 'accept':
295                                             $participant_status = ';PARTSTAT=ACCEPTED';
296                                             break;
297                                         case 'decline':
298                                             $participant_status = ';PARTSTAT=DECLINED';
299                                             break;
300                                         case 'tentative':
301                                             $participant_status = ';PARTSTAT=TENTATIVE';
302                                             break;
303                                     }
304                                 }
305                                 $str .= 'ATTENDEE'.$participant_status.';CN="'.$attendee->get_summary_text().'":mailto:'. (!empty($attendee->email1) ? $attendee->email1 : 'none@none.tld') . "\n";
306                             }
307                         }
308                     }
309                 }
310                 if (isset($event->reminder_time) && $event->reminder_time > 0 && $event->status != "Held")
311                 {
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";
317                 }
318                 $str .= "END:VEVENT\n";
319             }
320
321         }
322
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);
327
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))
335         {
336             foreach($projectTaskList as $projectTask)
337             {
338                 $str .= $this->createSugarIcalTodo($user_bean, $projectTask, "ProjectTask", $dtstamp);
339             }
340         }
341
342         if ($taskAsVTODO) {
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))
350             {
351                 foreach($taskList as $task)
352                 {
353                     $str .= $this->createSugarIcalTodo($user_bean, $task, "Tasks", $dtstamp);
354                 }
355             }
356         }
357         
358         return $str;
359     }
360
361     /**
362     * Gets the time zone for the given user.
363     *
364     * @param User $current_user the user
365     * @return DateTimeZone the user's timezone
366     */
367     protected function getUserTimezone($current_user)
368     {
369         $gmtTZ = new DateTimeZone("UTC");
370         $userTZName = TimeDate::userTimezone($current_user);
371         if (!empty($userTZName))
372         {
373             $tz = new DateTimeZone($userTZName);
374         } else
375         {
376             $tz = $gmtTZ;
377         }
378         return $tz;
379     }
380
381     /**
382     * Gets the daylight savings range for the given user.
383     *
384     * @param User $current_user the user
385     * @param integer $year the year
386     * @return array the start and end transitions of daylight savings
387     */
388     protected function getDSTRange($current_user, $year)
389     {
390         $tz = $this->getUserTimezone($current_user);
391         $idx = 0;
392         $result = array();
393
394         if (version_compare(PHP_VERSION, '5.3.0') >= 0)
395         {
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);
402
403             $transitions = $tz->getTransitions($year_date->getTimestamp(), $year_end->getTimestamp());
404             foreach($transitions as $transition) {
405                 if($transition['isdst']) {
406                     break;
407                 }
408                 $idx++;
409             }
410         } else {
411             $transitions = $tz->getTransitions();
412
413             $idx = 0;
414             foreach($transitions as $transition) {
415                 if($transition['isdst'] && intval(substr($transition["time"], 0, 4)) == intval(date("Y"))) {
416                     break;
417                 }
418                 $idx++;
419             }
420         }
421
422         if (empty($transitions[$idx]["isdst"])) {
423             // No DST transitions found
424             return $result;
425         }
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];
431         }
432         return $result;
433     }
434
435     /**
436     * Gets the timezone string for the current user.
437     *
438     * @return string the full timezone definition including daylight savings for the iCal
439     */
440     protected function getTimezoneString()
441     {
442         global $current_user, $timedate;
443         $timezoneName = $current_user->getPreference('timezone');
444
445         $gmtTZ = new DateTimeZone("UTC");
446         $tz = $this->getUserTimezone($current_user);
447         $dstRange = $this->getDSTRange($current_user, date('Y'));
448
449         $dstOffset = 0;
450         $gmtOffset = 0;
451
452         $timezoneString = "BEGIN:VTIMEZONE\n";
453         $timezoneString .= "TZID:" . $timezoneName . "\n";
454         $timezoneString .= "X-LIC-LOCATION:" . $timezoneName . "\n";
455
456         if (array_key_exists('start', $dstRange))
457         {
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";
466         }
467
468         if (array_key_exists('end', $dstRange))
469         {
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";
478         }
479
480         $timezoneString .= "END:VTIMEZONE\n";
481
482         return $timezoneString;
483     }
484
485     /**
486     * Generates the complete string for the calendar
487     *
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
491     */
492     function getVcalIcal(&$user_focus, $num_months)
493     {
494         global $current_user, $timedate;
495         $current_user = $user_focus;
496
497         $cal_name = $user_focus->first_name. " ". $user_focus->last_name;
498
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";
506
507         $now_date_time = $timedate->getNow(true);
508
509         global $sugar_config;
510         $timeOffset = 2;
511         if (isset($sugar_config['vcal_time']) && $sugar_config['vcal_time'] != 0 && $sugar_config['vcal_time'] < 13)
512         {
513             $timeOffset = $sugar_config['vcal_time'];
514         }
515         if (!empty($num_months))
516         {
517             $timeOffset = $num_months;
518         }
519         $start_date_time = $now_date_time->get("-$timeOffset months");
520         $end_date_time = $now_date_time->get("+$timeOffset months");
521
522         $utc_now_time = $this->getUtcDateTime($now_date_time);
523
524         $str .= $this->createSugarIcal($user_focus,$start_date_time,$end_date_time,$utc_now_time);
525
526         $str .= "DTSTAMP:" . $utc_now_time . "\n";
527         $str .= "END:VCALENDAR\n";
528
529         return $str;
530     }
531
532 }