1<?php
2/* Copyright (C) 2008-2011 Laurent Destailleur  <eldy@users.sourceforge.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18
19/**
20 *  \file       htdocs/core/lib/xcal.lib.php
21 *  \brief      Function to manage calendar files (vcal/ical/...)
22 */
23
24/**
25 *	Build a file from an array of events
26 *  All input params and data must be encoded in $conf->charset_output
27 *
28 *  @param      string  $format             "vcal" or "ical"
29 *  @param      string  $title              Title of export
30 *  @param      string  $desc               Description of export
31 *  @param      array   $events_array       Array of events ("uid","startdate","duration","enddate","title","summary","category","email","url","desc","author")
32 *  @param      string  $outputfile         Output file
33 *  @return     int                         < 0 if ko, Nb of events in file if ok
34 */
35function build_calfile($format, $title, $desc, $events_array, $outputfile)
36{
37	global $conf, $langs;
38
39	dol_syslog("xcal.lib.php::build_calfile Build cal file ".$outputfile." to format ".$format);
40
41	if (empty($outputfile))
42	{
43		// -1 = error
44		return -1;
45	}
46
47	// Note: A cal file is an UTF8 encoded file
48	$calfileh = fopen($outputfile, "w");
49
50	if ($calfileh)
51	{
52		include_once DOL_DOCUMENT_ROOT."/core/lib/date.lib.php";
53
54		$now      = dol_now();
55		$encoding = "";
56
57		if ($format === "vcal")
58		{
59			$encoding = "ENCODING=QUOTED-PRINTABLE:";
60		}
61
62		// Print header
63		fwrite($calfileh, "BEGIN:VCALENDAR\n");
64
65		// version is always "2.0"
66		fwrite($calfileh, "VERSION:2.0\n");
67
68		fwrite($calfileh, "METHOD:PUBLISH\n");
69		fwrite($calfileh, "PRODID:-//DOLIBARR ".DOL_VERSION."\n");
70		fwrite($calfileh, "CALSCALE:GREGORIAN\n");
71		fwrite($calfileh, "X-WR-CALNAME:".$encoding.format_cal($format, $title)."\n");
72		fwrite($calfileh, "X-WR-CALDESC:".$encoding.format_cal($format, $desc)."\n");
73		//fwrite($calfileh,"X-WR-TIMEZONE:Europe/Paris\n");
74
75		if (!empty($conf->global->MAIN_AGENDA_EXPORT_CACHE) && $conf->global->MAIN_AGENDA_EXPORT_CACHE > 60)
76		{
77			$hh = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "hour");
78			$mm = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "min");
79			$ss = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "sec");
80
81			fwrite($calfileh, "X-PUBLISHED-TTL: P".$hh."H".$mm."M".$ss."S\n");
82		}
83
84		foreach ($events_array as $key => $event)
85		{
86			// See http://fr.wikipedia.org/wiki/ICalendar for format
87			// See http://www.ietf.org/rfc/rfc2445.txt for RFC
88
89			// TODO: avoid use extra event array, use objects direct thahtwas created before
90
91			$uid           = $event["uid"];
92			$type          = $event["type"];
93			$startdate     = $event["startdate"];
94			$duration      = $event["duration"];
95			$enddate       = $event["enddate"];
96			$summary       = $event["summary"];
97			$category      = $event["category"];
98			$priority      = $event["priority"];
99			$fulldayevent  = $event["fulldayevent"];
100			$location      = $event["location"];
101			$email         = $event["email"];
102			$url           = $event["url"];
103			$transparency  = $event["transparency"];
104			$description   = dol_string_nohtmltag(preg_replace("/<br[\s\/]?>/i", "\n", $event["desc"]), 0);
105			$created       = $event["created"];
106			$modified      = $event["modified"];
107			$assignedUsers = $event["assignedUsers"];
108
109			// Format
110			$summary     = format_cal($format, $summary);
111			$description = format_cal($format, $description);
112			$category    = format_cal($format, $category);
113			$location    = format_cal($format, $location);
114
115			// Output the vCard/iCal VEVENT object
116			/*
117			Example from Google ical export for a 1 hour event:
118			BEGIN:VEVENT
119			DTSTART:20101103T120000Z
120			DTEND:20101103T130000Z
121			DTSTAMP:20101121T144902Z
122			UID:4eilllcsq8r1p87ncg7vc8dbpk@google.com
123			CREATED:20101121T144657Z
124			DESCRIPTION:
125			LAST-MODIFIED:20101121T144707Z
126			LOCATION:
127			SEQUENCE:0
128			STATUS:CONFIRMED
129			SUMMARY:Tâche 1 heure
130			TRANSP:OPAQUE
131			END:VEVENT
132
133			Example from Google ical export for a 1 day event:
134			BEGIN:VEVENT
135			DTSTART;VALUE=DATE:20101102
136			DTEND;VALUE=DATE:20101103
137			DTSTAMP:20101121T144902Z
138			UID:d09t43kcf1qgapu9efsmmo1m6k@google.com
139			CREATED:20101121T144607Z
140			DESCRIPTION:
141			LAST-MODIFIED:20101121T144607Z
142			LOCATION:
143			SEQUENCE:0
144			STATUS:CONFIRMED
145			SUMMARY:Tâche 1 jour
146			TRANSP:TRANSPARENT
147			END:VEVENT
148			*/
149
150			if ($type === "event")
151			{
152				fwrite($calfileh, "BEGIN:VEVENT\n");
153				fwrite($calfileh, "UID:".$uid."\n");
154
155				if (!empty($email))
156				{
157					fwrite($calfileh, "ORGANIZER:MAILTO:".$email."\n");
158					fwrite($calfileh, "CONTACT:MAILTO:".$email."\n");
159				}
160
161				if (!empty($url))
162				{
163					fwrite($calfileh, "URL:".$url."\n");
164				}
165
166				if (is_array($assignedUsers))
167				{
168					foreach ($assignedUsers as $assignedUser)
169					{
170						if ($assignedUser->email === $email)
171						{
172							continue;
173						}
174
175						fwrite($calfileh, "ATTENDEE;RSVP=TRUE:mailto:".$assignedUser->email."\n");
176					}
177				}
178
179				if ($created)
180				{
181					fwrite($calfileh, "CREATED:".dol_print_date($created, "dayhourxcard", true)."\n");
182				}
183
184				if ($modified)
185				{
186					fwrite($calfileh, "LAST-MODIFIED:".dol_print_date($modified, "dayhourxcard", true)."\n");
187				}
188
189				fwrite($calfileh, "SUMMARY:".$encoding.$summary."\n");
190				fwrite($calfileh, "DESCRIPTION:".$encoding.$description."\n");
191
192				if (!empty($location))
193				{
194					fwrite($calfileh, "LOCATION:".$encoding.$location."\n");
195				}
196
197				if ($fulldayevent)
198				{
199					fwrite($calfileh, "X-FUNAMBOL-ALLDAY:1\n");
200				}
201
202				// see https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/0f262da6-c5fd-459e-9f18-145eba86b5d2
203				if ($fulldayevent)
204				{
205					fwrite($calfileh, "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\n");
206				}
207
208				// Date must be GMT dates
209				// Current date
210				fwrite($calfileh, "DTSTAMP:".dol_print_date($now, "dayhourxcard", true)."\n");
211
212				// Start date
213				$prefix     = "";
214				$startdatef = dol_print_date($startdate, "dayhourxcard", true);
215
216				if ($fulldayevent)
217				{
218					// Local time
219					$prefix     = ";VALUE=DATE";
220					$startdatef = dol_print_date($startdate, "dayxcard", false);
221				}
222
223				fwrite($calfileh, "DTSTART".$prefix.":".$startdatef."\n");
224
225				// End date
226				if ($fulldayevent)
227				{
228					if (empty($enddate))
229					{
230						$enddate = dol_time_plus_duree($startdate, 1, "d");
231					}
232				} else {
233					if (empty($enddate))
234					{
235						$enddate = $startdate + $duration;
236					}
237				}
238
239				$prefix   = "";
240				$enddatef = dol_print_date($enddate, "dayhourxcard", true);
241
242				if ($fulldayevent)
243				{
244					$prefix   = ";VALUE=DATE";
245					$enddatef = dol_print_date($enddate + 1, "dayxcard", false);
246
247					// Local time
248					//$enddatef .= dol_print_date($enddate+1,"dayhourxcard",false);
249				}
250
251				fwrite($calfileh, "DTEND".$prefix.":".$enddatef."\n");
252				fwrite($calfileh, "STATUS:CONFIRMED\n");
253
254				if (!empty($transparency))
255				{
256					fwrite($calfileh, "TRANSP:".$transparency."\n");
257				}
258
259				if (!empty($category))
260				{
261					fwrite($calfileh, "CATEGORIES:".$encoding.$category."\n");
262				}
263
264				fwrite($calfileh, "END:VEVENT\n");
265			}
266
267			// Output the vCard/iCal VJOURNAL object
268			if ($type === "journal")
269			{
270				fwrite($calfileh, "BEGIN:VJOURNAL\n");
271				fwrite($calfileh, "UID:".$uid."\n");
272
273				if (!empty($email))
274				{
275					fwrite($calfileh, "ORGANIZER:MAILTO:".$email."\n");
276					fwrite($calfileh, "CONTACT:MAILTO:".$email."\n");
277				}
278
279				if (!empty($url))
280				{
281					fwrite($calfileh, "URL:".$url."\n");
282				}
283
284				if ($created)
285				{
286					fwrite($calfileh, "CREATED:".dol_print_date($created, "dayhourxcard", true)."\n");
287				}
288
289				if ($modified)
290				{
291					fwrite($calfileh, "LAST-MODIFIED:".dol_print_date($modified, "dayhourxcard", true)."\n");
292				}
293
294				fwrite($calfileh, "SUMMARY:".$encoding.$summary."\n");
295				fwrite($calfileh, "DESCRIPTION:".$encoding.$description."\n");
296				fwrite($calfileh, "STATUS:CONFIRMED\n");
297				fwrite($calfileh, "CATEGORIES:".$category."\n");
298				fwrite($calfileh, "LOCATION:".$location."\n");
299				fwrite($calfileh, "TRANSP:OPAQUE\n");
300				fwrite($calfileh, "CLASS:CONFIDENTIAL\n");
301				fwrite($calfileh, "DTSTAMP:".dol_print_date($startdatef, "dayhourxcard", true)."\n");
302
303				fwrite($calfileh, "END:VJOURNAL\n");
304			}
305		}
306
307		// Footer
308		fwrite($calfileh, "END:VCALENDAR");
309
310		fclose($calfileh);
311
312		if (!empty($conf->global->MAIN_UMASK))
313		{
314			@chmod($outputfile, octdec($conf->global->MAIN_UMASK));
315		}
316	} else {
317		dol_syslog("xcal.lib.php::build_calfile Failed to open file ".$outputfile." for writing");
318		return -2;
319	}
320}
321
322/**
323 *  Build a file from an array of events.
324 *  All input data must be encoded in $conf->charset_output
325 *
326 *  @param      string	$format             "rss"
327 *  @param      string	$title              Title of export
328 *  @param      string	$desc               Description of export
329 *  @param      array	$events_array       Array of events ("uid","startdate","summary","url","desc","author","category") or Array of WebsitePage
330 *  @param      string	$outputfile         Output file
331 *  @param      string	$filter             (optional) Filter
332 *  @param		string	$url				Url (If empty, forge URL for agenda RSS export)
333 *  @param		string	$langcode			Language code to show in header
334 *  @return     int                         < 0 if ko, Nb of events in file if ok
335 */
336function build_rssfile($format, $title, $desc, $events_array, $outputfile, $filter = '', $url = '', $langcode = '')
337{
338	global $user, $conf, $langs;
339	global $dolibarr_main_url_root;
340
341	dol_syslog("xcal.lib.php::build_rssfile Build rss file ".$outputfile." to format ".$format);
342
343	if (empty($outputfile))
344	{
345		 // -1 = error
346		return -1;
347	}
348
349	$fichier = fopen($outputfile, "w");
350
351	if ($fichier)
352	{
353		$date = date("r");
354
355		// Print header
356		fwrite($fichier, '<?xml version="1.0" encoding="'.$langs->charset_output.'"?>');
357		fwrite($fichier, "\n");
358
359		fwrite($fichier, '<rss version="2.0">');
360		fwrite($fichier, "\n");
361
362		fwrite($fichier, "<channel>\n");
363		fwrite($fichier, "<title>".$title."</title>\n");
364		if ($langcode) fwrite($fichier, "<language>".$langcode."</language>\n");
365
366		/*
367        fwrite($fichier, "<description><![CDATA[".$desc.".]]></description>"."\n".
368                // "<language>fr</language>"."\n".
369                "<copyright>Dolibarr</copyright>"."\n".
370                "<lastBuildDate>".$date."</lastBuildDate>"."\n".
371                "<generator>Dolibarr</generator>"."\n");
372        */
373
374		if (empty($url)) {
375			// Define $urlwithroot
376			$urlwithouturlroot = preg_replace("/".preg_quote(DOL_URL_ROOT, "/")."$/i", "", trim($dolibarr_main_url_root));
377			$urlwithroot       = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
378			//$urlwithroot=DOL_MAIN_URL_ROOT;                       // This is to use same domain name than current
379
380			$url = $urlwithroot."/public/agenda/agendaexport.php?format=rss&exportkey=".urlencode($conf->global->MAIN_AGENDA_XCAL_EXPORTKEY);
381		}
382
383		fwrite($fichier, "<link><![CDATA[".$url."]]></link>\n");
384
385		foreach ($events_array as $key => $event)
386		{
387			$eventqualified = true;
388
389			if ($filter)
390			{
391				// TODO Add a filter
392
393				$eventqualified = false;
394			}
395
396			if ($eventqualified)
397			{
398				if (is_object($event) && get_class($event) == 'WebsitePage') {
399					// Convert object into an array
400					$tmpevent = array();
401					$tmpevent['uid'] = $event->id;
402					$tmpevent['startdate'] = $event->date_creation;
403					$tmpevent['summary'] = $event->title;
404					$tmpevent['url'] = $event->fullpageurl ? $event->fullpageurl : $event->pageurl.'.php';
405					$tmpevent['author'] = $event->author_alias ? $event->author_alias : 'unknown';
406					//$tmpevent['category'] = '';
407					$tmpevent['desc'] = $event->description;
408
409					$event = $tmpevent;
410				}
411
412				$uid		  = $event["uid"];
413				$startdate	  = $event["startdate"];
414				$summary  	  = $event["summary"];
415				$url		  = $event["url"];
416				$author = $event["author"];
417				$category = $event["category"];
418
419				/* No place inside a RSS
420                $priority     = $event["priority"];
421                $fulldayevent = $event["fulldayevent"];
422                $location     = $event["location"];
423                $email        = $event["email"];
424                */
425
426				$description = dol_string_nohtmltag(preg_replace("/<br[\s\/]?>/i", "\n", $event["desc"]), 0);
427
428				fwrite($fichier, "<item>\n");
429				fwrite($fichier, "<title><![CDATA[".$summary."]]></title>\n");
430				fwrite($fichier, "<link><![CDATA[".$url."]]></link>\n");
431				fwrite($fichier, "<author><![CDATA[".$author."]]></author>\n");
432				fwrite($fichier, "<category><![CDATA[".$category."]]></category>\n");
433				fwrite($fichier, "<description><![CDATA[");
434
435				if ($description)
436					fwrite($fichier, $description);
437				// else
438				//     fwrite($fichier, "NoDesc");
439
440				fwrite($fichier, "]]></description>\n");
441				fwrite($fichier, "<pubDate>".date("r", $startdate)."</pubDate>\n");
442				fwrite($fichier, "<guid isPermaLink=\"true\"><![CDATA[".$uid."]]></guid>\n");
443				fwrite($fichier, "<source><![CDATA[Dolibarr]]></source>\n");
444				fwrite($fichier, "</item>\n");
445			}
446		}
447
448		fwrite($fichier, "</channel>");
449		fwrite($fichier, "\n");
450		fwrite($fichier, "</rss>");
451
452		fclose($fichier);
453
454		if (!empty($conf->global->MAIN_UMASK))
455		{
456			@chmod($outputfile, octdec($conf->global->MAIN_UMASK));
457		}
458	}
459}
460
461/**
462 *  Encode for cal export
463 *
464 *  @param      string  $format     "vcal" or "ical"
465 *  @param      string  $string     String to encode
466 *  @return     string              String encoded
467 */
468function format_cal($format, $string)
469{
470	global $conf;
471
472	$newstring = $string;
473
474	if ($format === "vcal")
475	{
476		$newstring = quotedPrintEncode($newstring);
477	}
478
479	if ($format === "ical")
480	{
481		// Replace new lines chars by "\n"
482		$newstring = preg_replace("/\r\n/i", "\\n", $newstring);
483		$newstring = preg_replace("/\n\r/i", "\\n", $newstring);
484		$newstring = preg_replace("/\n/i", "\\n", $newstring);
485
486		// Must not exceed 75 char. Cut with "\r\n"+Space
487		$newstring = calEncode($newstring);
488	}
489
490	return $newstring;
491}
492
493/**
494 *  Cut string after 75 chars. Add CRLF+Space.
495 *  line must be encoded in UTF-8
496 *
497 *  @param      string    $line     String to convert
498 *  @return     string              String converted
499 */
500function calEncode($line)
501{
502	$out     = "";
503	$newpara = "";
504
505	// If mb_ functions exists, it"s better to use them
506	if (function_exists("mb_strlen"))
507	{
508		$strlength = mb_strlen($line, "UTF-8");
509
510		for ($j = 0; $j < $strlength; $j++)
511		{
512			// Take char at position $j
513			$char = mb_substr($line, $j, 1, "UTF-8");
514
515			if ((mb_strlen($newpara, "UTF-8") + mb_strlen($char, "UTF-8")) >= 75)
516			{
517				// CRLF + Space for cal
518				$out .= $newpara."\r\n ";
519
520				$newpara = "";
521			}
522
523			$newpara .= $char;
524		}
525
526		$out .= $newpara;
527	} else {
528		$strlength = dol_strlen($line);
529
530		for ($j = 0; $j < $strlength; $j++)
531		{
532			// Take char at position $j
533			$char = substr($line, $j, 1);
534
535			if ((dol_strlen($newpara) + dol_strlen($char)) >= 75)
536			{
537				// CRLF + Space for cal
538				$out .= $newpara."\r\n ";
539
540				$newpara = "";
541			}
542
543			$newpara .= $char;
544		}
545
546		$out .= $newpara;
547	}
548
549	return trim($out);
550}
551
552
553/**
554 *  Encode into vcal format
555 *
556 *  @param      string  $str        String to convert
557 *  @param      int     $forcal     (optional) 1 = For cal
558 *  @return     string              String converted
559 */
560function quotedPrintEncode($str, $forcal = 0)
561{
562	$lines = preg_split("/\r\n/", $str);
563	$out   = "";
564
565	foreach ($lines as $line)
566	{
567		$newpara = "";
568
569		// Do not use dol_strlen here, we need number of bytes
570		$strlength = strlen($line);
571
572		for ($j = 0; $j < $strlength; $j++)
573		{
574			$char  = substr($line, $j, 1);
575			$ascii = ord($char);
576
577			if ($ascii < 32 || $ascii === 61 || $ascii > 126)
578			{
579				$char = "=".strtoupper(sprintf("%02X", $ascii));
580			}
581
582			// Do not use dol_strlen here, we need number of bytes
583			if ((strlen($newpara) + strlen($char)) >= 76)
584			{
585				// New line with carray-return (CR) and line-feed (LF)
586				$out .= $newpara."=\r\n";
587
588				// extra space for cal
589				if ($forcal)
590					$out .= " ";
591
592				$newpara = "";
593			}
594
595			$newpara .= $char;
596		}
597
598		$out .= $newpara;
599	}
600	return trim($out);
601}
602
603/**
604 *  Decode vcal format
605 *
606 *  @param      string  $str    String to convert
607 *  @return     string          String converted
608 */
609function quotedPrintDecode($str)
610{
611	return trim(quoted_printable_decode(preg_replace("/=\r?\n/", "", $str)));
612}
613