<?php /* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Furkan Mustafa, 2015.04.06 - Updated 2015.04.09: Limit lines to 70 chars (spec is 75) - Updated 2015.04.26: duplicate letter fixed by @PGallagher69 (Peter Gallagher) - Updated 2015.04.26: Outtlook Invite fixed by @PGallagher69 (Peter Gallagher) - Updated 2015.05.02: Line-limit bug fixed by @waddyvic (Victor Huang) Alec M., 2022.02.05 - Updated 2022.02.05: General readability improvements, cleanup, and add namespace Adapted from: https://gist.github.com/jakebellacera/635416 Also see: https://www.ietf.org/rfc/rfc5545.txt */ namespace amattu; /** * ICS class */ class SimpleICS { use SimpleICS_Util; /** * ICS file mime type * * @var string */ public const MIME_TYPE = 'text/calendar; charset=utf-8'; /** * ICS event ID prefix * * NOTE: * (1) This can be a empty string * * @var string */ public const EVENT_PREFIX = 'SICS'; /** * An array of events in the ICS file * * @var array */ protected $events = []; /** * A string containing the ICS file generator string * * @var string */ protected $productString = '-//amattu/SimpleICS//NONSGML v1.0//EN'; /** * A string containing the ICS file template * * @var string */ public static $Template = null; /** * Class Constructor * * @param string $productString */ public function __construct($productString = null) { if ($productString) { $this->productString = $productString; } } /** * Add a new event to the ICS file * * @param array|SimpleICS_Event $eventOrClosure * @return SimpleICS_Event */ public function addEvent($eventOrClosure) : SimpleICS_Event { // Instantiate ICS event class if (is_object($eventOrClosure) && ($eventOrClosure instanceof \Closure)) { $event = new SimpleICS_Event(); $eventOrClosure($event); } // Push Event to Array $this->events[] = $event; return $event; } /** * Turn the ICS event array into a string * * @return string */ public function serialize() : string { return $this->filter_linelimit($this->render(self::$Template, $this)); } } /** * ICS event class */ class SimpleICS_Event { use SimpleICS_Util; /** * Event ID * * @var string */ public $uniqueId; /** * Event Start DateTime * * @var \DateTime */ public $startDate; /** * Event End DateTime * * @var \DateTime */ public $endDate; /** * Event TimeStamp * * @var \DateTime */ public $dateStamp; /** * Event Location * * @var string */ public $location; /** * Event Description * * @var string */ public $description; /** * Event URL/URI * * @var string */ public $uri; /** * Event Summary (Title) * * @var string */ public $summary; /** * Event ICS Template * * @var string */ public static $Template; /** * Class Constructor */ public function __construct() { $this->uniqueId = uniqid(SimpleICS::EVENT_PREFIX); } /** * Turn the ICS event into a string * * @return string */ public function serialize() : string { return $this->render(self::$Template, $this); } } /** * SimpleICS Utility Trait */ trait SimpleICS_Util { /** * Shorten each input line to specified limit * * @param string $input * @param ?integer $lineLimit * @return string $output */ function filter_linelimit($input, $lineLimit = 70) { // Variables $output = ''; $line = ''; $pos = 0; // Iterate over string while ($pos < strlen($input)) { // Find newlines $newLinepos = strpos($input, "\n", $pos + 1); if (!$newLinepos) $newLinepos = strlen($input); $line = substr($input, $pos, $newLinepos - $pos); if (strlen($line) <= $lineLimit) { $output .= $line; } else { // First line cut-off limit is $lineLimit $output .= substr($line, 0, $lineLimit); $line = substr($line, $lineLimit); // Subsequent line cut-off limit is $lineLimit - 1 due to the leading white space $output .= "\n " . substr($line, 0, $lineLimit - 1); while (strlen($line) > $lineLimit - 1){ $line = substr($line, $lineLimit - 1); $output .= "\n " . substr($line, 0, $lineLimit - 1); } } $pos = $newLinepos; } return $output; } /** * Format the event date * * @param \DateTime|string $input * @return string $output formatted date */ function filter_calDate($input) : string { if (!is_a($input, 'DateTime')) { $input = new \DateTime($input); } else { $input = clone $input; } // Format and return $input->setTimezone(new \DateTimeZone('UTC')); return $input->format('Ymd\THis\Z'); } /** * Convert various input types to a string * * @param string $input * @return string $output */ function filter_serialize($input) { if (is_object($input)) { return $input->serialize(); } if (is_array($input)) { $output = ''; array_walk($input, function($item) use (&$output) { $output .= $this->filter_serialize($item); }); return trim($output, "\r\n"); } return $input; } /** * Escape quotes in a string * * @param string $input * @return string $output */ function filter_quote($input) { return quoted_printable_encode($input); } /** * Escape content from a string * * @param string $input * @return string $output */ function filter_escape($input) { $input = preg_replace('/([\,;])/','\\\$1', $input); $input = str_replace("\n", "\\n", $input); $input = str_replace("\r", "\\r", $input); return $input; } function render($tpl, $scope) { while (preg_match("/\{\{([^\|\}]+)((?:\|([^\|\}]+))+)?\}\}/", $tpl, $m)) { $replace = $m[0]; $varname = $m[1]; $filters = isset($m[2]) ? explode('|', trim($m[2], '|')) : []; $value = $this->fetch_var($scope, $varname); $self = &$this; array_walk($filters, function(&$item) use (&$value, $self) { $item = trim($item, "\t\r\n "); if (!is_callable([ $self, 'filter_' . $item ])) throw new \Exception('No such filter: ' . $item); $value = call_user_func_array([ $self, 'filter_' . $item ], [ $value ]); }); $tpl = str_replace($m[0], $value, $tpl); } return $tpl; } function fetch_var($scope, $var) { if (strpos($var, '.')!==false) { $split = explode('.', $var); $var = array_shift($split); $rest = implode('.', $split); $val = $this->fetch_var($scope, $var); return $this->fetch_var($val, $rest); } if (is_object($scope)) { $getterMethod = 'get' . ucfirst($var); if (method_exists($scope, $getterMethod)) { return $scope->{$getterMethod}(); } return $scope->{$var}; } if (is_array($scope)) return $scope[$var]; throw new \Exception('A strange scope'); } } /** * ICS Content Template * * @var string */ SimpleICS::$Template = <<<EOT BEGIN:VCALENDAR VERSION:2.0 PRODID:{{productString}} METHOD:PUBLISH CALSCALE:GREGORIAN {{events|serialize}} END:VCALENDAR EOT; /** * ICS Event Content Template * * @var string */ SimpleICS_Event::$Template = <<<EOT BEGIN:VEVENT UID:{{uniqueId}} DTSTART:{{startDate|calDate}} DTSTAMP:{{dateStamp|calDate}} DTEND:{{endDate|calDate}} LOCATION:{{location|escape}} DESCRIPTION:{{description|escape}} URL;VALUE=URI:{{uri|escape}} SUMMARY:{{summary|escape}} END:VEVENT EOT;