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