person holding pen and using a tablet

Fixing iCalendar feeds

The local government here has all the schools use an iCalendar feed for things like when school terms start and stop and other school events occur. The department’s website also has events like public holidays. The issue is that all of them don’t make it an all-day event but one that happens at midnight, or one past midnight.

The events synchronise fine, though Google’s calendar is known for synchronising when it feels like it, not at any particular time you would like it to.

Screenshot of Android Calendar showing a tiny bar at midnight which is the event.

Even though a public holiday is all day, they are sent as appointments for midnight.

That means on my phone all the events are these tiny bars that appear right up the top of the screen and are easily missed, especially when the focus of the calendar is during the day.

On the phone, you can see the tiny purple bar at midnight. This is how the events appear. It’s not the calendar’s fault, as far as it knows the school events are happening at midnight.

You can also see Lunar New Year and Australia Day appear in the all-day part of the calendar and don’t scroll away. That’s where these events should be.

Why are all the events appearing at midnight? The reason is the feed is incorrectly set up and has the time. The events are sent in an iCalendar format and a typical event looks like this:

BEGIN:VEVENT
DTSTART;TZID=Australia/Sydney:20230206T000000
DTEND;TZID=Australia/Sydney:20230206T000000
SUMMARY:School Term starts
END:VEVENT

The event starting and stopping date and time are the DTSTART and DTEND lines. Both of them have the date of 2023/02/06 or 6th February 2023 and a time of 00:00:00 or midnight. So the calendar is doing the right thing, we need to fix the feed!

The Fix

I wrote a quick and dirty PHP script to download the feed from the real site, change the DTSTART and DTEND lines to all-day events and leave the rest of it alone.

<?php
$site = $_GET['s'];
if ($site == 'site1') {
    $REMOTE_URL='https://site1.example.net/ical_feed';
} elseif ($site == 'site2') {
    $REMOTE_URL='https://site2.example.net/ical_feed';
} else {
    http_response_code(400);
    die();
}

$fp = fopen($REMOTE_URL, "r");
if (!$fp) {
    die("fopen");
}
header('Content-Type: text/calendar');
while (( $line = fgets($fp, 1024)) !== false) {
    $line = preg_replace(
        '/^(DTSTART|DTEND);[^:]+:([0-9]{8})T000[01]00/',
        '${1};VALUE=DATE:${2}',
        $line);
    echo $line;
}
?>

It’s pretty quick and nasty but gets the job done. So what is it doing?

  • Lines 2-10: Check the given variable s and match it to either “site1” or “site2” to obtain the URL. If you only had one site to fix you could just set the REMOTE_URL variable.
  • Lines 12-15: A typical fopen() and nasty error handling.
  • Line 16: set the content type to a calendar.
  • Line 17: A while loop to read the contents of the remote site line by line.
  • Line 18-21: This is where the “magic” happens, preg_replace is a Perl regular expression replacement. The PCRE is:
    • Finding lines starting with DTSTART or DTEND and store it in capture 1
    • Skip everything that isn’t a colon. This is the timezone information. I wasn’t sure if it was needed and how to combine it so I took it out. All the all-day events I saw don’t have a time zone.
    • Find 8 numerics (this is for YYYYMMDD) and store it in capture 2.
    • Scan the Time part, a literal “T” then HHMMSS. Some sites use midnight some use one minute past, so it covers both.
    • Replace the line with either DTSTART or DTEND (capture 1), set the value type to DATE as the default is date/time and print the date (capture 2).
  • Line 22: Print either the modified or original line.

You need to save the script on your web server somewhere, possibly with an alias command.

The whole point of this is to change the type from a date/time to a date-only event and only print the date part of it for the start and end of it. The resulting iCalendar event looks like this:

BEGIN:VEVENT
DTSTART;VALUE=DATE:20230206
DTEND;VALUE=DATE:20230206
SUMMARY:School Term starts
END:VEVENT

The calendar then shows it properly as an all-day event. I would check the script works before doing the next step. You can use things like curl or wget to download it. If you use a normal browser, it will probably just download the translated file.

If you’re not seeing the right thing then it’s probably the PCRE failing. You can check it online with a regex checker such as https://regex101.com. The site has saved my PCRE and match so you got something to start with.

Calendar settings

The last thing to do is to change the URL in your calendar settings. Each calendar system has a different way of doing it. For Google Calendar they provide instructions and you want to follow the section titled “Use a link to add a public Calendar”.

The URL here is not the actual site’s URL (which you would have put into the REMOTE_URL variable before) but the URL of your script plus the ‘?s=site1″ part. So if you put your script aliased to /myical.php and the site ID was site1 and your website is www.example.com the URL would be “https://www.example.com/myical.php?s=site1”.

You should then see the events appear as all-day events on your calendar.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *