I've solved this with a little help from my friends and iCalcreator. Code as follows:
<?php
/*
To begin with, you will need an access token. This is complicated stuff - getting a regular one is easy, but we want one that lasts permanently. As described in Scenario 5 here: https://developers.facebook.com/roadmap/offline-access-removal/ , we need to:
1. Get a regular short-lived access token
2. Exchange it for a long-lived access token
3. Using our new access token, get a permanent page access token.
First, create a new Facebook app at https://developers.facebook.com/apps/ . It can be named whatever, it's not going to be really used. Fill in a domain - it doesn't matter which, just take one that shows simple HTML and doesn't do any redirects: I used my own static web page, http://jobjorn.se/ , but http://example.org/ may work too.
As instructed by MPaulo on Stack Overflow here: https://stackoverflow.com/a/11238327/564628 , use the app id - from your newly created app - and visit this page:
https://www.facebook.com/dialog/oauth?client_id=MY_APP_ID&redirect_uri=MY_SITE_URL&scope=manage_pages&response_type=token
Be sure to replace "MY_APP_ID" and "MY_SITE_URL".
Then, exchange your short-lived access token for a long-lived access token by visiting this page:
https://graph.facebook.com/oauth/access_token?client_id=MY_APP_ID&client_secret=MY_APP_SECRET&grant_type=fb_exchange_token&fb_exchange_token=YOUR_ACCESS_TOKEN
Be sure to replace "MY_APP_ID", "MY_APP_SECRET", and "YOUR_ACCESS_TOKEN" (the last being the access token you received from the first page)
Finally, get your page access token here:
https://graph.facebook.com/me/accounts?access_token=YOUR_NEW_ACCESS_TOKEN
Be sure to replace "YOUR_NEW_ACCESS_TOKEN" (the access token you recieved from the second page)
Paste it below. It should be permanent, but due to a bug it might only last two months - see https://developers.facebook.com/bugs/151056591697025 for details. It seems to have been fixed however. You can test it here: https://developers.facebook.com/tools/debug/access_token
*/
// You will need to edit these variables.
$access_token = "";
$page = "";
// We don't want to query the Facebook Graph API over and over, so we'll cache our results. You can force the cache to update by visiting the script and appending "?f=true", otherwise it will only run (rather than display the cache) if the cache is older than 3600 seconds (one hour).
$cache = $page . ".cache";
$f = false;
if($_GET['f'] == "true"){
$f = true;
}
if(!file_exists($cache) || filemtime($cache) <= time()-3600 || $f){
// Get and decode the data - your page's event list - from the API
$graph_url = "https://graph.facebook.com/" . $page . "/events?access_token=" . $access_token;
$data = file_get_contents($graph_url);
$data = json_decode($data);
if(!empty($data->error)){
echo '<b>$data error</b><br />';
echo $data->error->type . " error (" . $data->error->code . "/" . $data->error->error_subcode . "): " . $data->error->message;
exit;
}
// Go through the list of events, and get and decode more detailed information on each event.
foreach ($data->data as $event){
$event_data = file_get_contents("https://graph.facebook.com/" . $event->id . "?access_token=" . $access_token);
$event_data = json_decode($event_data);
if(!empty($event_data->error)){
echo '<b>$event_data error</b><br />';
echo $event_data->error->type . " error (" . $event_data->error->code . "/" . $event_data->error->error_subcode . "): " . $event_data->error->message;
exit;
}
// Store it in an array for later use.
$events[] = $event_data;
}
// We're now done fetching the data from Facebook's Graph API. Now we'll have to create an iCal file.
// This requires the iCalcreator PHP class, which you can downloead from kigkonsult at kigkonsult.se/iCalcreator/index.php
require_once("icalcreator/iCalcreator.class.php");
// Create the calendar, set up some basic properties
$c = new vcalendar(array('unique_id' => $page));
$c->setProperty('X-WR-CALNAME', $page . ' events');
$c->setProperty('X-WR-CALDESC', 'Facebook events for ' . $page);
$c->setProperty('X-WR-TIMEZONE', $events[0]->timezone); // We assume all of the events use the same timezone.
// Loop through the events, create event components in the calendar
foreach($events as $key => $event){
$e[$key] = & $c->newComponent('vevent');
$e[$key]->setProperty('summary', $event->name);
$e[$key]->setProperty('dtstart', $event->start_time);
$e[$key]->setProperty('dtend', $event->end_time);
$e[$key]->setProperty('description', $event->description . "\n\nhttp://www.facebook.com/events/" . $event->id);
$e[$key]->setProperty('location', $event->location);
}
// Remove the cache if it exists
if(file_exists($cache)){
unlink($cache);
}
// Open (create) the cache file
if(!$handle = fopen($cache, 'w')){
echo "Cannot open output file: " . $cache;
exit;
}
// Write the calendar to the cache
if(fwrite($handle, $c->createCalendar()) === FALSE){
echo "Cannot write to output file: " . $cache;
exit;
}
// Close the cache file
fclose($handle);
}
// Now we've got the calendar in the cache file, either newly generated and stored there just a few lines ago or from earlier. Now we'll just display it.
header("Content-Type: text/calendar; charset=UTF-8");
header("Content-Disposition: filename=" . $page . ".ics");
require($cache);
?>