Skip to content

Instantly share code, notes, and snippets.

@edewaal97
Forked from seebz/iCal.php
Last active February 23, 2024 20:35
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save edewaal97/344ec108931ac4c9e8ca7874b2db83cb to your computer and use it in GitHub Desktop.
Save edewaal97/344ec108931ac4c9e8ca7874b2db83cb to your computer and use it in GitHub Desktop.
iCal PHP Parser
<?php
// SOURCE: https://gist.github.com/seebz/c00a38d9520e035a6a8c
class iCal
{
/**
* @var string
*/
public $title;
/**
* @var string
*/
public $description;
/**
* @var array
*/
public $events = array();
/**
* @var array
*/
protected $_eventsByDate;
public function __construct($content = null)
{
if ($content) {
$isUrl = strpos($content, 'http') === 0 && filter_var($content, FILTER_VALIDATE_URL);
$isFile = strpos($content, "\n") === false && file_exists($content);
if ($isUrl || $isFile) {
$content = file_get_contents($content);
}
$this->parse($content);
}
}
public function title()
{
return $this->summary;
}
public function description()
{
return $this->description;
}
public function events()
{
return $this->events;
}
public function eventsByDate()
{
if (! $this->_eventsByDate) {
$this->_eventsByDate = array();
$tmpEventsByDate = array();
foreach ($this->events() as $event) {
foreach ($event->occurrences() as $occurrence) {
$date = date('Y-m-d', $occurrence);
$newevent = clone $event;
$newevent->fixOccurringDate($occurrence);
// generate key for sorting
$key = strtotime($newevent->dateStart);
while(isset($tmpEventsByDate[$date][$key])) $key++;
$tmpEventsByDate[$date][$key] = $newevent;
}
}
// sort array
ksort($tmpEventsByDate);
foreach ($tmpEventsByDate as $date => $value) {
ksort($value);
$this->_eventsByDate[$date] = $value;
}
// prevent duplicates for edited dates in recurring events
foreach ($this->_eventsByDate as $dateKey => $date) {
foreach ($date as $event) {
if(!empty($event->recurrenceId)) {
$uid = $event->uid;
foreach ($date as $eventKey => $eventValue) {
if($eventValue->uid == $uid && (empty($eventValue->recurrenceId))) {
unset($this->_eventsByDate[$dateKey][$eventKey]);
}
}
}
}
}
}
return $this->_eventsByDate;
}
public function eventsByDateBetween($start, $end, int $limit=NULL)
{
if ((string) (int) $start !== (string) $start) {
$start = strtotime($start);
}
$start = date('Y-m-d', $start);
if ((string) (int) $end !== (string) $end) {
$end = strtotime($end);
}
$end = date('Y-m-d', $end);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date && $date < $end) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function eventsByDateSince($start, int $limit=NULL)
{
if ((string) (int) $start !== (string) $start) {
$start = strtotime($start);
}
$start = date('Y-m-d', $start);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function eventsByDateUntil($end, int $limit=NULL)
{
if ((string) (int) $end !== (string) $end) {
$end = strtotime($end);
}
$start = date('Y-m-d');
$end = date('Y-m-d', $end);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date && $end >= $date) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function parse($content)
{
$content = str_replace("\r\n ", '', $content);
// Title
preg_match('`^X-WR-CALNAME:(.*)$`m', $content, $m);
$this->title = $m ? trim($m[1]) : null;
// Description
preg_match('`^X-WR-CALDESC:(.*)$`m', $content, $m);
$this->description = $m ? trim($m[1]) : null;
// Events
preg_match_all('`BEGIN:VEVENT(.+)END:VEVENT`Us', $content, $m);
foreach ($m[0] as $c) {
$this->events[] = new iCal_Event($c);
}
return $this;
}
}
class iCal_Event
{
/**
* @var string
*/
public $uid;
/**
* @var string
*/
public $summary;
/**
* @var string
*/
public $description;
/**
* @var string
*/
public $dateStart;
/**
* @var string
*/
public $dateEnd;
/**
* @var string
*/
public $recurrenceId;
/**
* @var array
*/
public $exdate = array();
/**
* @var stdClass
*/
public $recurrence;
/**
* @var string
*/
public $location;
/**
* @var string
*/
public $status;
/**
* @var string
*/
public $created;
/**
* @var string
*/
public $updated;
/**
* @var integer
*/
protected $_timeStart;
/**
* @var integer
*/
protected $_timeEnd;
/**
* @var integer
*/
protected $_recurrenceId;
/**
* @var array
*/
protected $_occurrences;
public function __construct($content = null)
{
if ($content) {
$this->parse($content);
}
}
public function summary()
{
return $this->summary;
}
public function title()
{
return $this->summary;
}
public function description()
{
return $this->description;
}
public function occurrences()
{
if (empty($this->_occurrences)) {
$this->_occurrences = $this->_calculateOccurrences();
}
return $this->_occurrences;
}
public function duration()
{
// if ($this->_timeEnd) {
return $this->_timeEnd - $this->_timeStart;
// }
}
public function parse($content)
{
$content = str_replace("\r\n ", '', $content);
// UID
if (preg_match('`^UID:(.*)$`m', $content, $m))
$this->uid = trim($m[1]);
// Summary
if (preg_match('`^SUMMARY:(.*)$`m', $content, $m))
$this->summary = trim($m[1]);
// Description
if (preg_match('`^DESCRIPTION:(.*)$`m', $content, $m))
$this->description = trim($m[1]);
// Date start
if (preg_match('`^DTSTART(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_timeStart = strtotime($m[1]);
$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);
}
// Date end
if (preg_match('`^DTEND(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_timeEnd = strtotime($m[1]);
$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
}
// Recurrence-Id
if (preg_match('`^RECURRENCE-ID(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_recurrenceId = strtotime($m[1]);
$this->recurrenceId = date('Y-m-d H:i:s', $this->_recurrenceId);
}
// Exdate
if (preg_match_all('`^EXDATE(;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
foreach ($m[2] as $dates) {
$dates = explode(',', $dates);
foreach ($dates as $d) {
$this->exdate[] = date('Y-m-d', strtotime($d));
}
}
}
// Recurrence
if (preg_match('`^RRULE:(.*)`m', $content, $m)) {
$rules = (object) array();
$rule = trim($m[1]);
$rule = explode(';', $rule);
foreach ($rule as $r) {
list($key, $value) = explode('=', $r);
$rules->{ strtolower($key) } = $value;
}
if (isset($rules->until)) {
$rules->until = date('Y-m-d H:i:s', strtotime($rules->until));
}
if (isset($rules->count)) {
$rules->count = intval($rules->count);
}
if (isset($rules->interval)) {
$rules->interval = intval($rules->interval);
}
if (isset($rules->byday)) {
$rules->byday = explode(',', $rules->byday);
}
// Avoid infinite recurrences
if (! isset($rules->until) && ! isset($rules->count)) {
$rules->count = 500;
}
$this->recurrence = $rules;
}
// Location
if (preg_match('`^LOCATION:(.*)$`m', $content, $m))
$this->location = trim($m[1]);
// Status
if (preg_match('`^STATUS:(.*)$`m', $content, $m))
$this->status = trim($m[1]);
// Created
if (preg_match('`^CREATED:(.*)`m', $content, $m))
$this->created = date('Y-m-d H:i:s', strtotime(trim($m[1])));
// Updated
if (preg_match('`^LAST-MODIFIED:(.*)`m', $content, $m))
$this->updated = date('Y-m-d H:i:s', strtotime(trim($m[1])));
return $this;
}
public function isRecurrent()
{
return ! empty($this->recurrence);
}
public function fixOccurringDate($timestamp)
{
if($timestamp != $this->_timeStart) {
// calculate correct start & end date if not a repeating event
$duration = $this->duration();
// get date from occurrences
$timestampCalc = new DateTime();
$timestampCalc->setTimestamp($timestamp);
// make new startdate and start timestamp
$startCalc = new DateTime();
$startCalc->setTimestamp($this->_timeStart);
$startCalc->setDate($timestampCalc->format('Y'), $timestampCalc->format('m'), $timestampCalc->format('d'));
$this->_timeStart = $startCalc->getTimestamp();
$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);
// calculate end date and time with duration of original event.
$this->_timeEnd += - $this->_timeStart + $duration;
$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
}
}
protected function _isExdate($date)
{
if ((string) (int) $date != $date) {
$date = strtotime($date);
}
$date = date('Y-m-d', $date);
return in_array($date, $this->exdate);
}
protected function _calculateOccurrences()
{
$occurrences = array($this->_timeStart);
if ($this->isRecurrent())
{
$freq = $this->recurrence->freq;
$count = isset($this->recurrence->count) ? $this->recurrence->count : null;
$until = isset($this->recurrence->until) ? strtotime($this->recurrence->until) : null;
$callbacks = array(
'YEARLY' => '_nextYearlyOccurrence',
'MONTHLY' => '_nextMonthlyOccurrence',
'WEEKLY' => '_nextWeeklyOccurrence',
'DAILY' => '_nextDailyOccurrence'
);
$callback = $callbacks[$freq];
$offset = $this->_timeStart;
$continue = $until ? ($offset < $until) : ($count > 1);
while ($continue) {
if(isset($occurrence)) {
if (! $this->_isExdate($occurrence)) {
$occurrences[] = $occurrence;
$count--;
}
}
$occurrence = $this->{$callback}($offset);
$offset = $occurrence;
$continue = $until ? ($offset < $until) : ($count > 1);
}
}
if ($this->_isExdate($occurrences[0])) {
unset($occurrences[0]);
$occurrences = array_values($occurrences);
}
return $occurrences;
}
protected function _nextYearlyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
return strtotime("+{$interval} year", $offset);
}
protected function _nextMonthlyOccurrence($offset)
{
$dayname = array(
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday'
);
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
// INTERVAL IS BY (COUNT)DAYNAME
if(isset($this->recurrence->byday)){
$dates = array();
foreach ($this->recurrence->byday as $pattern) {
$offsetDateTime = new DateTime();
$offsetDateTime->setTimestamp((int) $offset);
preg_match('`([-]?\d+)?(MO|TU|WE|TH|FR|SA|SU)`m', $pattern, $m);
$recurrenceOffset = (isset($m[1])) ? (int) $m[1] : 1;
$recurrenceDay = strtr($m[2], $dayname);
$forDateTime = clone $offsetDateTime;
for (
$month = (int) $offsetDateTime->format('Ym');
$month <= date('Ym', strtotime('+' . $interval*12 . ' months'));
$month = (int) $forDateTime->modify('+'.$interval.' months')->format('Ym')
) {
$yearMonth = $forDateTime->format('Y-m');
$firstDay = new DateTime('first '. $recurrenceDay . ' of ' . $yearMonth);
$lastDay = new DateTime('last '. $recurrenceDay . ' of ' . $yearMonth);
$newDate = $firstDay;
$daysInMonth = array();
while ($newDate->getTimestamp() <= $lastDay->getTimestamp()) {
$daysInMonth[] = $newDate->getTimestamp();
$newDate->modify('next '. $recurrenceDay);
}
if($recurrenceOffset < 0) {
$dates[] = $daysInMonth[count($daysInMonth) + $recurrenceOffset];
} else {
$dates[] = $daysInMonth[$recurrenceOffset - 1];
}
}
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
// INTERVAL IS BY DAYNUMBER OF MONTH
$bymonthday = isset($this->recurrence->bymonthday)
? explode(',', $this->recurrence->bymonthday)
: array(date('d', $offset));
$start = strtotime(date('Y-m-01 H:i:s', $offset));
$dates = array();
foreach ($bymonthday as $day) {
// this month
$dates[] = strtotime(($day-1) . ' day', $start);
// next 'interval' month
$tmp = strtotime("+{$interval} month", $start);
$time = strtotime(($day-1) . ' day', $tmp);
if ((string) (int) date('d', $time) == (int) $day) {
$dates[] = $time;
}
// 2x 'interval' month
$interval *= 2;
$tmp = strtotime("+{$interval} month", $start);
$time = strtotime(($day-1) . ' day', $tmp);
if ((string) (int) date('d', $time) === (int) $day) {
$dates[] = $time;
}
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
protected function _nextWeeklyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
$byday = isset($this->recurrence->byday)
? $this->recurrence->byday
: array( substr(strtoupper(date('D', $offset)), 0, 2) );
$start = date('l', $offset) !== 'Monday'
? strtotime('last monday', $offset)
: $offset;
$daysname = array(
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday',
);
$dates = array();
foreach ($byday as $day) {
$dayname = $daysname[$day];
// this week
$dates[] = strtotime($dayname, $start);
// next 'interval' week
$tmp = strtotime("+{$interval} week", $start);
$time = strtotime($dayname, $tmp);
$dates[] = $time;
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
protected function _nextDailyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
return strtotime("+{$interval} day", $offset);
}
}
<?php
include 'iCal.php';
$file = 'http://www.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics';
$iCal = new iCal($file);
$events = $iCal->eventsByDate();
// or :
// $events = $iCal->eventsByDateBetween('2014-01-01', '2015-01-01');
// or :
// $events = $iCal->eventsByDateSince('2014-01-01');
// or :
// $events = $iCal->eventsByDateSince('today');
// or :
// $events = $iCal->eventsByDateUntil('+30 days');
// in addition, every function starting with eventsBy... can be extended with another variable with a limit on the amount of events.
// for example:
// $events = $iCal->eventsByDateUntil('+30 days', 10);
foreach ($events as $date => $events)
{
echo $date . "\n";
echo '----------' . "\n";
foreach ($events as $event)
{
echo '* ' . $event->title() . "\n";
}
echo "\n";
}
@TCOA
Copy link

TCOA commented Feb 13, 2020

Class works fine on example, but missing some code to handle the calendars I tested.....

Fixed one bug....

in fixOccurringDate($timestamp) you have
$this->_timeEnd - $this->_timeStart + $duration;
should be
$this->_timeEnd = $this->_timeEnd - $this->_timeStart + $duration;

Added needed features
in $callbacks, add to the array:
'DAILY' => '_nextDailyOccurrence'

also needs the function.....

protected function _nextDailyOccurrence($offset) { $interval = isset($this->recurrence->interval) ? $this->recurrence->interval : 1; return strtotime("+{$interval} day", $offset); }

and a needed function (IMHO - and the one I will likely use the most...) is

public function eventsByDateUntil($end) { if ((string)(int)$end !== (string)$end) { $end = strtotime($end); } $start = date('Y-m-d'); $end = date('Y-m-d', $end); $return = array(); foreach ($this->eventsByDate() as $date => $events) { if ($start <= $date && $end >= $date) { $return[$date] = $events; } } return $return; }å

with a useage of

$events = $iCal->eventsByDateUntil('+30 days');

Another feature that would be good is to add a $limit to each 'eventsBy....' function that would allow only returning X number of events.

So, for example, my function becomes:

public function eventsByDateUntil($end, $limit='') { if ((string)(int)$end !== (string)$end) { $end = strtotime($end); } $start = date('Y-m-d'); $end = date('Y-m-d', $end); $return = array(); foreach ($this->eventsByDate() as $date => $events) { if ($start <= $date && $end >= $date) { if(empty($limit) || count($return) <= $limit){ $return[$date] = $events; } } if(!empty($limit) && count($return) >= $limit){ break; } } return $return; }

So things like recurring birthdays don't get out of hand....... ;)

with a useage of

$events = $iCal->eventsByDateUntil('+30 days');
is still OK (returns everything within the next 30 days)
or
$events = $iCal->eventsByDateUntil('+30 days', 10);
limits the data to only the next 10 things happening within the next 30 days (good for "heads up" displays and such)

@edewaal97
Copy link
Author

@TCOA, good call. I didn't need it at the time but it's a good addition. I added your updates for future use. Thanks for the feedback!

@ThosG
Copy link

ThosG commented Apr 18, 2020

@edewaal97 Thank you for this. Could you help me with usage? My php segment simply includes your file 'usage.php', which looks as though it should cause some output, but nothing happens.

@ThosG
Copy link

ThosG commented Apr 18, 2020

Oh dear. I'm very sorry. I forgot to use the .php extension.

@cinnamon17
Copy link

Is there some method to add a new event??

@edewaal97
Copy link
Author

@cinnamon17 Unfortunately that is not possible currently. You can add events by adding them to the calendar using the program for the source (Google calendar in this example). Otherwise you are free to fork it and code it yourself. Unfortunately there is no way to push the new event back to the source. That's a limitation of the api/ical specification.

@bitnulleins
Copy link

bitnulleins commented Jan 9, 2021

@edewaal97 Correct regex in line 349 and 353 for utf-8 encoding. It requires a regex for form: SUMMATY ;CHARSET=utf-8 : ...
Here's my fork.

@sissad
Copy link

sissad commented Jan 25, 2021

Hi,
Thank you for this Script! Ist Very helpfull!
How can i diplay Start time of the event?
Thanks
Sissad

@wodim
Copy link

wodim commented Feb 14, 2021

@edewaal97 I think a nice addition for the iCal class would be a function that returns the current event, if any.

@mdhassanemam
Copy link

@edewaal97 I am new on this i need the available dates not reserved date, I am trying to find the property by between two dates where property is available.

@philbert25
Copy link

I would like to output something like this:
<li><a href="link to event in calendar">Title of Event - Date of Event using Month, Day format</a></li>
This would need to output the next 3 events from today forward.
So far I have been able to do it all except for the link and reformatting the date. Is this possible? Thank you.

@Kcko
Copy link

Kcko commented Sep 19, 2021

i have it

@philbert25
Copy link

i have it

How were you able to get the url of the event? Also did you see my comment about reformatting the date? Is that something you could help me with?

@Kcko
Copy link

Kcko commented Sep 21, 2021

i have it

How were you able to get the url of the event? Also did you see my comment about reformatting the date? Is that something you could help me with?

I added new functionality which can do it. Here is my version with example:
https://gist.github.com/Kcko/901b82682bcd14a511b1bb4939cd5543

Is needed to add identificator of your calendar as a second argument (see example).
Your question about date - It belongs to the basic operations in PHP, what exactly do you not know?

@philbert25
Copy link

@Kcko For some reason your fork doesn't work getting the URL for me.

As far as the reformatting the date, I need to change the date('Y-m-d') to date('F j'). I have tried replacing using all instances or individual instances, and it either does nothing or breaks the page. I have also tried adding $date = date('F j', $date); to my code, but that didn't work either.

Here is the code I am using to output what I want.
$events = $iCal->eventsByDateUntil('+30 days', 3);
foreach ($events as $date => $events)

{
foreach ($events as $event)
{
echo "

  • " . $event->title() . " - " . $date . "
  • ";
    }
    }

    The other I am getting is that I should only be outputting 3 events, but if the date is the same, it doesn't count them into the total. Here is an example:

    • Last day to withdraw with “W” grade - First half block - 2021-09-24
    • Free GED class Orientation - Du Quoin morning - 2021-09-28
    • Free GED class Orientation - Murphysboro Afternoon - 2021-09-28
    • Student Senate meeting - 2021-09-28
    • PTK Honor Society meeting - 2021-09-28
    • Honor Society Information Session - Phi Theta Kappa Honor Society - 2021-09-29

    I am getting 6 returned, I believe because 4 have the same date.

    Thanks in advance for any help.

    @Kcko
    Copy link

    Kcko commented Sep 24, 2021

    @Kcko For some reason your fork doesn't work getting the URL for me.

    As far as the reformatting the date, I need to change the date('Y-m-d') to date('F j'). I have tried replacing using all instances or individual instances, and it either does nothing or breaks the page. I have also tried adding $date = date('F j', $date); to my code, but that didn't work either.

    Here is the code I am using to output what I want.
    $events = $iCal->eventsByDateUntil('+30 days', 3);
    foreach ($events as $date => $events)

    {
    foreach ($events as $event)
    {
    echo "

    • " . $event->title() . " - " . $date . "
      ";
      }
      }
      The other I am getting is that I should only be outputting 3 events, but if the date is the same, it doesn't count them into the total. Here is an example:

    • Last day to withdraw with “W” grade - First half block - 2021-09-24

    • Free GED class Orientation - Du Quoin morning - 2021-09-28

    • Free GED class Orientation - Murphysboro Afternoon - 2021-09-28

    • Student Senate meeting - 2021-09-28

    • PTK Honor Society meeting - 2021-09-28

    • Honor Society Information Session - Phi Theta Kappa Honor Society - 2021-09-29

    I am getting 6 returned, I believe because 4 have the same date.

    Thanks in advance for any help.

    Hi,

    The simplest date conversion:

    $date = '2021-09-24';
    $newFormat = new \DateTime($date);
    echo $newFormat->format('F j');
    

    My fork works fine :)
    You're probablyy making a mistake somewhere, show me how to use it

    @mlongoria1
    Copy link

    Anyone else having trouble with recurring events using this class?
    I have an event that starts on 2021-11-03 14:00:00 and ends at 2021-11-03 17:00:00(3hr long event) and recurs 'weekly' for two additional weeks.

    When I parse the recurring event I get this back:

    First event in the series - reports correctly
    startdate of 2021-11-03 14:00:00
    enddate of 2021-11-03 17:00:00

    Second event: - incorrect
    startdate of 2021-11-10 14:00:00
    enddate of 1969-12-24 23:00:00

    Third event: - incorrect
    startdate of 2021-11-17 14:00:00
    enddate of 1969-12-17 23:00:00

    Does anyone know why I am receiving a 1969 date??? The start date is incrementing by 7 days each week, so what's going on with enddate?

    Any help would be appreciated, thanks!

    @mlongoria1
    Copy link

    @edewaal97

    It turns out that the problem was on line 436.

    You need to set the $this->_timeStart variable to a temp variable before adding it to duration else you will run into issues as I described above. The logic is sound but the original implementation was incorrect.

    Here is the fix(this allows timestart to be added to duration correctly.)

    $ts = $this->_timeStart;
    $this->_timeEnd = $ts + $duration;
    

    Hope this helps someone looking at this class in the future.

    Thank you,

    ML

    @leoandrews
    Copy link

    Thanks @mlongoria1 - that was flummoxing me too!

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