Skip to content

Instantly share code, notes, and snippets.

@amattu2
Forked from furkanmustafa/SimpleICS.php
Last active March 27, 2023 09:27
Show Gist options
  • Save amattu2/e2d3ee4a122052210ee9b56de6620a09 to your computer and use it in GitHub Desktop.
Save amattu2/e2d3ee4a122052210ee9b56de6620a09 to your computer and use it in GitHub Desktop.
A simple ICS (iCalendar) event generator class.
<?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;
<?php
/*
* Produced: Sat Feb 05 2022
* Author: Alec M.
* GitHub: https://amattu.com/links/github
* Copyright: (C) 2022 Alec M.
* License: License GNU Affero General Public License v3.0
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Require the ICS class
require(__DIR__ . "/SimpleICS.class.php");
// Instantiate the ICS class
$cal = new amattu\SimpleICS();
// Add a new event
$cal->addEvent(function($e) {
$e->startDate = new DateTime();
$e->endDate = (new DateTime())->add(new DateInterval('PT130M'));
$e->summary = 'Business Meeting';
$e->description = 'Lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet';
$e->uri = 'https://example.com';
$e->location = '1600 Pennsylvania Avenue, N.W., Washington, DC 20500';
});
// Add another event
/*
$cal->addEvent(function($e) {
$e->startDate = new DateTime();
$e->endDate = (new DateTime())->add(new DateInterval('PT130M'));
$e->summary = 'Personal Meeting';
$e->description = 'Bla bla bla';
$e->uri = 'https://meeting.com';
$e->location = 'https://zoom.us/j/123456789';
});
*/
// Output the file
if (!empty($_GET) && isset($_GET['download'])) {
header('Content-Type: ' . amattu\SimpleICS::MIME_TYPE);
header('Content-Disposition: inline; filename=event.ics');
} else {
echo "<pre>";
}
echo $cal->serialize();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment