Skip to content

Instantly share code, notes, and snippets.

@furkanmustafa
Last active May 16, 2023 12:42
Show Gist options
  • Star 35 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save furkanmustafa/d9f62450bf7db9b825e3 to your computer and use it in GitHub Desktop.
Save furkanmustafa/d9f62450bf7db9b825e3 to your computer and use it in GitHub Desktop.
Simple ICS Generation class for PHP
<?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)
Adapted from: https://gist.github.com/jakebellacera/635416
Also see: https://www.ietf.org/rfc/rfc5545.txt
Development Sponsored by 77hz KK, Tokyo, http://77hz.jp
Usage:
$cal = new SimpleICS();
// $cal->productString = '-//77hz/iFLYER API//';
$cal->addEvent(function($e) {
$e->startDate = new DateTime("2015-04-06T10:00:00+09:00");
$e->endDate = new DateTime("2015-04-06T18:30:00+09:00");
$e->uri = 'http://url.to/my/event';
$e->location = 'Tokyo, Event Location';
$e->description = 'ICS Entertainment';
$e->summary = 'Lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet';
});
header('Content-Type: '.SimpleICS::MIME_TYPE);
if (isset($_GET['download'])) {
header('Content-Disposition: attachment; filename=event.ics');
}
echo $cal->serialize();
*/
class SimpleICS {
use SimpleICS_Util;
const MIME_TYPE = 'text/calendar; charset=utf-8';
var $events = [];
var $productString = '-//hacksw/handcal//NONSGML v1.0//EN';
static $Template = null;
function addEvent($eventOrClosure) {
if (is_object($eventOrClosure) && ($eventOrClosure instanceof Closure)) {
$event = new SimpleICS_Event();
$eventOrClosure($event);
}
$this->events[] = $event;
return $event;
}
function serialize() {
return $this->filter_linelimit($this->render(self::$Template, $this));
}
}
class SimpleICS_Event {
use SimpleICS_Util;
var $uniqueId;
var $startDate;
var $endDate;
var $dateStamp;
var $location;
var $description;
var $uri;
var $summary;
static $Template;
function __construct() {
$this->uniqueId = uniqid();
}
function serialize() {
return $this->render(self::$Template, $this);
}
}
trait SimpleICS_Util {
function filter_linelimit($input, $lineLimit = 70) {
// go through each line and make them shorter.
$output = '';
$line = '';
$pos = 0;
while ($pos < strlen($input)) {
// find the newline
$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;
}
function filter_calDate($input) {
if (!is_a($input, 'DateTime'))
$input = new DateTime($input);
else
$input = clone $input;
$input->setTimezone(new DateTimeZone('UTC'));
return $input->format('Ymd\THis\Z');
}
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;
}
function filter_quote($input) {
return quoted_printable_encode($input);
}
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');
}
}
SimpleICS::$Template = <<<EOT
BEGIN:VCALENDAR
VERSION:2.0
PRODID:{{productString}}
METHOD:PUBLISH
CALSCALE:GREGORIAN
{{events|serialize}}
END:VCALENDAR
EOT;
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;
@pjgpetecodes
Copy link

Duplicate Letters caused during splitting on Line 108.... Change line at 108 to;

$line = substr($line, $lineLimit);

To fix.

@pjgpetecodes
Copy link

Also, adding a new line "METHOD:PUBLISH" after line 193 "PRODID:{{productString}}", allows the invites to open on older versions of Outlook (2003 tested).

@furkanmustafa
Copy link
Author

@PGallagher69 Thanks! Integrated your fix&addition.

@waddyvic
Copy link

waddyvic commented May 1, 2015

Hi furkanmustafa,

I was using your SimpleICS class and found a bug. I've forked your code and fixed it. Please feel free to pull the changes back to your code.

@aaroncrawford
Copy link

For those having time zone issues - remove the \Z on line ~124 :

return $input->format('Ymd\THis');

\Z will return GMT.

@furkanmustafa
Copy link
Author

@waddyvic Thanks, merged your changes!
@aaroncrawford Thanks, I'll try to look for some nice way to make it configurable.

@mikejoyceio
Copy link

Awesome job. Thanks!

@kriscreative
Copy link

Hi,

I'm trying to use this code to create an event calendar for my website.
An ics file is generated, but none of the info passed is in there (only the default values are).
Simply echoing out the passed info before the addEvent call does output all the correct info.
What am I doing wrong?

Thanks for any help!

Code below:

$cal = new SimpleICS();
// $cal->productString = '-//77hz/iFLYER API//';

// Pull events from database
foreach($pages->find("template=event") as $event){

// Convert event start time that was pulled from database to format expected.
$temp = date("Y-m-d",$event->event_start)."T".date("H:i:sP",$event->event_start);

$cal->addEvent(function($e) {
    $e->startDate = new DateTime($temp);
    $e->endDate = new DateTime($temp);
    $e->location = $event->location;
    $e->description = $event->description;
    $e->summary = $event->summary;
});

header('Content-Type: '.SimpleICS::MIME_TYPE);
if (isset($_GET['download'])) {
    header('Content-Disposition: attachment; filename=event.ics');
}
echo $cal->serialize();

}

@furkanmustafa
Copy link
Author

@kriscreative sorry for late reply, this thing (gist) doesn't have notifications for some reason.

You need to use use keyword for that to work.

$cal->addEvent(function($e) use ($temp, $event) {

@helmerdavila
Copy link

Nice

👍

@nailsnglue
Copy link

I would really like to use this code on my website. I'm quite new so I am hoping someone can be kind.

I use a database which only prints to pdf various fields from my database and inside that pdf are appointment details. I want to format the Pdf with a link which includes appointment details which will populate the simpleICS.php function via post method which will then download an ics file for the appointmentee can use.

I presume that the code below goes inside my functions.php and the simpleics.php placed in directory but how do you call that function from the actual page I am send the data to in a url string.

$cal = new SimpleICS();
// $cal->productString = '-//77hz/iFLYER API//';
$cal->addEvent(function($e) {
$e->startDate = new DateTime("2015-04-06T10:00:00+09:00");
$e->endDate = new DateTime("2015-04-06T18:30:00+09:00");
$e->uri = 'http://url.to/my/event';
$e->location = 'Tokyo, Event Location';
$e->description = 'ICS Entertainment';
$e->summary = 'Lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet, lorem ipsum dolor ics amet';
});
header('Content-Type: '.SimpleICS::MIME_TYPE);
if (isset($_GET['download'])) {
header('Content-Disposition: attachment; filename=event.ics');
}
echo $cal->serialize();

@sumachaa
Copy link

Is there any way to include an attachment in the ICS file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment