view index.php @ 193:16ce445c499a v3 tip

Import v3 branch.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 22 Jan 2017 02:31:10 +0200
parents f2adb44ea251
children
line wrap: on
line source

<?php
//
// OAMK Lukkari v3.0
// (C) Copyright 2010 - 2015 Matti 'ccr' Hämäläinen <ccr@tnsp.org>
// Yes, this code is rather horrible. :|
//
// Include framework
require "mgeneric.inc.php";

// Default settings
$pageName = "OAMK Lukkari";
$pageVersion = "3.0alpha";
$mobileMode = FALSE;
$baseURI = "http://example.com/";

$pageLanguages = array("fi", "en");
$pageCSSData = array("cookie" => "lukcss", "prefix" => "luk");

$classDefaultID = "DEFAULT";
$classIDFile = array(FALSE => "classes.txt", TRUE => "classes_next.txt");
$courseCacheFile = "coursecache.txt";

if (file_exists("config.inc.php"))
  @require "config.inc.php";

//
// Hardcoded tables
//
$lukDayNames = array(
  "fi" => array("Maanantai", "Tiistai", "Keskiviikko", "Torstai", "Perjantai", "Lauantai", "Sunnuntai"),
  "en" => array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
);

$pageTranslations = array(
  "change" => array(
    "fi" => "Vaihda",
    "en" => "Change",
  ),

  "period_shown" => array(
    "fi" => "Näkyvillä",
    "en" => "Shown",
  ),
  "next_period" => array(
    "fi" => "Seuraava periodi",
    "en" => "Next period",
  ),
  "current_period" => array(
    "fi" => "Nykyinen periodi",
    "en" => "Current period",
  ),
  "expl_toggle_period" => array(
    "fi" => "Vaihda nykyisen ja seuraavan periodin (jos saatavilla) lukujärjestyksen välillä.",
    "en" => "Switch view between current and next period (if available) timetable.",
  ),

  "viikossa" => array(
    "fi" => "Viikossa yhteensä <b>%1</b> tuntia.",
    "en" => "Total of <b>%1</b> hours in the week.",
  ),

  "vuoroviikoin" => array(
    "fi" => "Vuoroviikoin",
    "en" => "Every other week",
  ),

  "class_not_set" => array(
    "fi" => "Luokkaa ei asetettu, käytetään vakioarvoa <b>%1</b>.",
    "en" => "Class not set, using default <b>%1</b>.",
  ),

  "class_data_not_found" => array(
    "fi" => "Luokan %1 tietoja ei löytynyt! Jos luokkakoodi on uusi, ".
    "ilmestyy se järjestelmään seuraavan päivityksen aikana. Luokkatiedot ".
    "päivitetään noin kerran viikossa. <b>On myös mahdollista, että luokkakoodi ".
    "on olemassa vain seuraavan periodin tiedoissa.</b>",

    "en" => "Data for class %1 was not found. If the class code is new, it ".
    "should appear in this system during the next update. The data is updated ".
    "approximately once per week. <b>It is also possible that the code only ".
    "exists for next period's data.</b>",
  ),
  
  "class_list_not_found" => array(
    "fi" => "Luokkien listaa ei löytynyt. Kokeile ladata sivu uudelleen hetken kuluttua.",
    "en" => "Class list not found. An update may be in progress, try reloading in a minute.",
  ),
  
  "class_format_error" => array(
    "fi" => "Virhe! Luokan täytyy olla muotoa <b>XXXnXXX</b>, käytetään vakioarvoa <b>%1</b>.",
    "en" => "Error! Class code must be of format <b>XXXnXXX</b>, using default value of <b>%1</b>.",
  ),

  "change_style" => array(
    "fi" => "Tyyli",
    "en" => "Style",
  ),

  "link_original_data" => array(
    "fi" => "Alkuperäinen",
    "en" => "Original",
  ),

  "expl_link_original_data" => array(
    "fi" => "Linkki alkuperäiseen lukujärjestysdataan.",
    "en" => "Link to the original time table data.",
  ),
  
  "link_mobile_version" => array(
    "en" => "Mobile",
    "fi" => "Mobile",
  ),
  
  "expl_link_mobile_version" => array(
    "fi" => "Yksinkertaisempi mobiililaiteversio lukujärjestyksestä.",
    "en" => "Simpler mobile device version of the timetable.",
  ),
  
  "link_current_settings" => array(
    "fi" => "Linkki",
    "en" => "Link",
  ),
  
  "expl_link_current_settings" => array(
    "fi" => "Linkki tähän lukujärjestykseen nykyisillä asetuksilla (kieli, jne.)",
    "en" => "Link to the this timetable with current settings (language, etc.)",
  ),

  "contact" => array(
    "fi" => "Yhteydenotot <b>%1</b> tai <b>%2</b>. ".
    "En vastaa mahdollisista virheistä tai epätarkkuuksista tiedoissa!",

    "en" => "Contact <b>%1</b> or <b>%2</b>. ".
    "The author can't be held responsible for any errors or inaccuracies in the data!",
  ),
  
  "updated" => array(
    "fi" => "Päivitetty: <b>%1</b>.",
    "en" => "Last updated: <b>%1</b>.",
  ),
  
  "beta" => array(
    "fi" => " HUOM! %2 v%1 on vielä kehityksen alla. Bugeja voi löytyä.",
    "en" => " NOTICE! %2 v%1 is still under development. There may be bugs.",
  ),

//  "" => array("fi" => ),
);


function lukGetDayName($day)
{
  global $lukDayNames, $pageLang;

  if (isset($lukDayNames[$pageLang]) && isset($lukDayNames[$pageLang][$day]))
    return $lukDayNames[$pageLang][$day];
  else
    return $lukDayNames["en"][$day];
}


function lukCheckClassID(&$id)
{
  global $classDefaultID;
  if (preg_match("#^([A-Z]{3}\d[A-Za-z0-9_]{1,6}|ccr|Ryh_[A-Z]{3}\d[A-Za-z0-9_]{1,6})$#", $id, $m))
  {
    $id = $m[1];
    return TRUE;
  } else {
    stError(cmQM("class_format_error", $classDefaultID));
    $id = $classDefaultID;
    return FALSE;
  }
}


function lukGetWeekdayFromTimestamp($stamp)
{
  $info = getdate($stamp);
  $day = $info["wday"];
  return ($day > 0) ? $day - 1 : $day + 6;
}


function lukGetHourStamp($stamp)
{
  return date("H:i", mktime(0, 0, $stamp, 0, 0, 0));
}


function lukGetHourStr($hour)
{
  global $classHourTimes;
  if (isset($classHourTimes[$hour]))
  {
    return
      lukGetHourStamp($classHourTimes[$hour]["start"]).
      " - ".
      lukGetHourStamp($classHourTimes[$hour]["end"]);
  }
  else
    return "ERROR";
}


function lukClassGetHourStr($start, $end)
{
  global $classHourTimes;
  return
    lukGetHourStamp($classHourTimes[$start]["start"]).
    " - ".
    lukGetHourStamp($classHourTimes[$end]["end"]);
}


function lukFetchCourseData($id, $uri, &$cache)
{
  global $pageCharset;

  if (($data = @file_get_contents($uri)) !== FALSE)
  {
    $data = @iconv("iso8859-15", $pageCharset, $data);

    // <td id="oj_nimi" class="smallheadercell"><strong>Korjausrakentamisen rakennussuunnittelu 3 op</strong></td>
    if (preg_match("#<td id=\"oj_nimi\" class=\"smallheadercell\"><strong>(.+?)\s+(\d+)\s*(op|ECTS\s+cr)\s*</strong></td>#", $data, $m))
    {
      $cache = array("desc" => trim($m[1]), "op" => intval($m[2]), "uri" => $uri);
      return TRUE;
    }
    else
    // <td><strong>... (N op)</strong></td>
    if (preg_match("#<td><strong>(.+?)\s+\((\d+)\s*(op|ECTS\s+cr|cr)\)\s*</strong></td>#i", $data, $m))
    {
      $cache = array("desc" => trim($m[1]), "op" => intval($m[2]), "uri" => $uri);
      return TRUE;
    }
  }
  return FALSE;
}


function lukMatchCourse($id)
{
  global $cache, $cacheDirty, $pageLang, $mobileMode;

  // Create the index
  if (!isset($cache[$id]))
    $cache[$id] = array();

  // Check if course exists in cache
  if (!isset($cache[$id][$pageLang]))
  {
    // Not cached, try to fetch data
    $uri = "http://www.oamk.fi/opinto-opas/opintojaksohaku/?sivu=oj_kuvaus&koodi1=".$id."&kieli=".strtoupper($pageLang);
    if (
        lukFetchCourseData($id, $uri."&opas=2014-2015", $cache[$id][$pageLang]) ||
        lukFetchCourseData($id, $uri."&opas=2015-2016", $cache[$id][$pageLang])
      )
      $cacheDirty = TRUE;
  }

  if (isset($cache[$id]) && isset($cache[$id][$pageLang]))
  {
    if ($mobileMode)
    {
      return "<b>".chentities($cache[$id][$pageLang]["desc"])."</b>";
    }
    else
    {
       return
         "<a target=\"_blank\" title=\"".chentities($id." - ".$cache[$id][$pageLang]["op"]." op").
         "\" href=\"".chentities($cache[$id][$pageLang]["uri"])."\">".chentities($cache[$id][$pageLang]["desc"])."</a>";
    }
  }
  else
    return chentities($id);
}


function lukGetClassInfo($class, $indent = "")
{
  $data = $class["data"];
  if ($class["grouped"])
  {
    $out = "";
    foreach ($data as $col)
    {
      $out .= 
        $indent."<div class=\"group\">\n".
        $indent." <div class=\"groupCell\">".lukMatchCourse($col[0])."</div>\n";

      for ($i = 1; $i < count($col); $i++)
      {
        $out .= 
          $indent." <div class=\"groupCell\">".
          (isset($col[$i]) ? chentities($col[$i]) : "").
          "</div>\n";
      }
      
      $out .= $indent."</div>\n";
    }

    if ($class["turns"])
      $out .= "<div class=\"groupCell eoWeekly\">".cmQM("vuoroviikoin")."</div>\n";

    return $out;
  }
  else
  {
    $out = $indent."<div class=\"groupCell\">".lukMatchCourse($data[0][0])."</div>\n";

    for ($i = 1; $i < count($data[0]); $i++)
    {
      $out .= $indent."<div class=\"groupCell\">".chentities($data[0][$i])."</div>\n";
    }

    if ($class["turns"])
      $out .= "<div class=\"groupCell eoWeekly\">".cmQM("vuoroviikoin")."</div>\n";

    return $out;
  }
}


function lukFindClass($day, $hour)
{
  global $classHourDefs, $classDayTable;
  if (isset($classDayTable[$day]))
  {
    foreach ($classDayTable[$day] as $id)
    {
      if ($hour >= $classHourDefs[$id]["start"] && 
          $hour <  $classHourDefs[$id]["start"] + $classHourDefs[$id]["hours"])
        return $id;
    }
  }
  return 0;
}


function lukReadClassFile($filename)
{
  // Attempt to open file for reading
  if (($fp = @fopen($filename, "rb")) === false)
    return false;

  $mclasses = FALSE;

  // Lock file so that we do not get clashes
  if (flock($fp, LOCK_SH))
  {
    $mclasses = array();
    // Read and parse data
    while (!feof($fp))
    {
      $str = trim(fgets($fp, 128));
      if (strlen($str) > 2 && $str[0] != "#")
        $mclasses[] = $str;
    }

    // Release lock
    flock($fp, LOCK_UN);
  }

  fclose($fp);
  
  sort($mclasses);
  return $mclasses;
}


function lukPrintTimeTable($mini)
{
  global $classInfo, $classHourDefs, $classHourTimes;
  
//  $currStamp = time() + ((3 * 60) + 45) * 60;
  $currStamp = time();
  $currTime = $currStamp - mktime(0, 0, 0);
  $currDay = lukGetWeekdayFromTimestamp($currStamp);

  $nextStamp = $currStamp + 30 * 60;
  $nextTime = $nextStamp - mktime(0, 0, 0);

  $out =
    "<div id=\"timeTable\">\n";
  
  // Create the timetable table
  if ($mini)
  {
    $startDay = $currDay;
    $lastDay = $currDay + 1;
  }
  else
  {
    $startDay = 0;
    $lastDay = $classInfo["maxDays"];
  }

  $out .=
    " <div class=\"timeTableHourList\" style=\"width: ".($mini ? "20%" : "10%").";\">\n".
    "  <div class=\"timeTableWeekday\">&nbsp;</div>\n";
  for ($hour = $classInfo["firstHour"]; $hour < $classInfo["lastHour"]; $hour++)
  {
    $out .= "  <div class=\"timeTableHourBox\"><div class=\"timeTableHour\">".lukGetHourStr($hour)."</div></div>\n";
  }
  $out .= " </div>\n";

  if ($mini)
    $tmpS = "style=\"width: 75%;\"";
  else
    $tmpS = sprintf("style=\"width: %1.3f%%;\"", 100 / ($lastDay - $startDay + 1));

  for ($day = $startDay; $day < $lastDay; $day++)
  {
    $out .=
      " <div class=\"timeTableDay".($day == $currDay ? " active" : "")."\" ".$tmpS.">\n".
      "  <div class=\"timeTableWeekday\">".lukGetDayName($day)."</div>\n";

    for ($hour = $classInfo["firstHour"]; $hour < $classInfo["lastHour"]; $hour++)
    {
      $id = lukFindClass($day, $hour);
      if ($id > 0)
      {
        $class = &$classHourDefs[$id];
        if (!isset($class["set"]))
        {
          $class["set"] = TRUE;

          $nextActive = $day == $currDay && 
                      $nextTime >= $classHourTimes[$class["start"]]["start"] &&
                      $nextTime <  $classHourTimes[$class["start"] + $class["hours"] - 1]["end"];

          $isActive = $day == $currDay && 
                      $currTime >= $classHourTimes[$class["start"]]["start"] &&
                      $currTime <  $classHourTimes[$class["start"] + $class["hours"] - 1]["end"];

          $out .= 
            "  <div class=\"classInfoBox\" style=\"height: ".($class["hours"] * 6)."em;\">\n".
            "   <div class=\"classInfo".
            ($isActive ? " clactive" : "").
            (!$isActive && $nextActive ? " clnext " : "").
            (($class["grouped"] || $class["turns"]) ? " clgrouped" : " clnormal")."\">\n".
            lukGetClassInfo($class, "    ").
            "    <div class=\"nhours\"><span>".lukClassGetHourStr($hour, $hour + $class["hours"] - 1)." (".$class["hours"]."h)</span></div>\n".
            "   </div>\n".
            "  </div>\n";
        }
      }
      else
      {
        $out .= "  <div class=\"classInfoBox\"><div class=\"classInfo clnothing\"></div></div>\n";
      }
    }
    $out .= " </div>\n";
  }

  return $out."</div>\n";
}



//
// Main code begins
//
// Check given parameters:
// Language must be the first setting to be validated,
// so that the translation support works properly.
//

if (($tmp = stGetRequestItem("lang", FALSE, TRUE)) !== FALSE)
{
  $tmp = strtolower($tmp);
  if (in_array($tmp, $pageLanguages))
  {
    $pageLang = $tmp;
    setcookie("luklang", $tmp, time() + 365*24*60*60); // expire in a year
  }
}
else
if (isset($_COOKIE["luklang"]))
{
  $tmp = $_COOKIE["luklang"];
  if (in_array($tmp, $pageLanguages))
    $pageLang = $tmp;
}


// Cookie info window
$showCookieInfo = isset($_COOKIE["lukcookieinfo"]) ? !$_COOKIE["lukcookieinfo"] : TRUE;


// Development info window
$showDevInfo = isset($_COOKIE["lukdevinfo"]) ? $_COOKIE["lukdevinfo"] : TRUE;
setcookie("lukdevinfo", 0, time() + 2*7*24*60*60);


// Check class setting (check "luokka" for backwards compatibility also)
if ((($classID = stGetRequestItem("class", FALSE, TRUE)) !== FALSE ||
     ($classID = stGetRequestItem("luokka", FALSE, TRUE)) !== FALSE) && lukCheckClassID($classID))
{
  setcookie("lukclass", $classID, time() + 365*24*60*60); // expire in a year
}
else
if (isset($_COOKIE["lukclass"]))
{
  $classID = $_COOKIE["lukclass"];
  lukCheckClassID($classID);
}
else
{
  stError(cmQM("class_not_set", $classDefaultID));
  $classID = $classDefaultID;
}


// Check next period flag
if (isset($_REQUEST["next"]))
{
  $nextPeriod = TRUE;
  $cachePath = "cache-next/";
}
else
{
  $nextPeriod = FALSE;
  $cachePath = "cache/";
}

// Get original base URI data
$origBaseURI = "";
if (file_exists($cachePath."baseuri.data"))
  require $cachePath."baseuri.data";


// Global cache for course data
$cache = array();
$cacheDirty = FALSE;

// Try to read cachefile, if we can get file lock on it
if (($fp = @fopen($courseCacheFile, "rb")) !== FALSE)
{
  if (flock($fp, LOCK_SH))
  {
    require($courseCacheFile);
    flock($fp, LOCK_UN);
  }
  fclose($fp);
}


// Read classfile
if (($classIDs = lukReadClassFile($classIDFile[$nextPeriod])) === false)
  stError(cmQM("class_list_not_found"));


// Read class data
$dataFile = $cachePath.$classID.".data";
if (!file_exists($dataFile))
{
  stError(cmQM("class_data_not_found", chentities($classID)));
  $haveData = FALSE;
  $timestamp = time();
}
else
{
  require($dataFile);
  $haveData = isset($classInfo);
  $timestamp = filemtime($dataFile);
}


// Create references to mobile device (Apple touch) icons
$extra = "";
foreach (array(57 => FALSE, 76 => TRUE, 114 => TRUE, 120 => TRUE, 152 => TRUE) as $iconSize => $addSize)
{
  $extra .= "  <link rel=\"apple-touch-icon\" ".
    ($addSize ? "sizes=\"".$iconSize."x".$iconSize."\" " : "").
    "href=\"img/icon-".$iconSize."-precomposed.png\" />\n";
}

// XXX: Temporarily no-index (remember also robots.txt!)
$extra .= "  <meta name=\"robots\" content=\"noindex\">\n";

// For mobile shit
$extra .= "  <meta name=\"viewport\" content=\"width=device-width\" />\n";

// Start printing the page
$pageTitle = $haveData ? $classID." / ".join("; ", $classInfo["info"]) : $classID;
cmPrintPageHeader($pageTitle." - ".$pageName, $extra);

echo "
<script type=\"text/javascript\">
function lukSetCookie(cname, cvalue, cctime)
{
  var de = new Date();
  de.setTime(de.getTime() + cctime*1000);
  document.cookie = cname +\"=\"+ cvalue +\"; expires=\"+ de.toUTCString();
}

function lukSetViewDo(elem, state)
{
  elem.style.display = state ? 'block' : 'none';
}

function lukSetView(id, state)
{
  var elem = document.getElementById(id);
  if (elem) lukSetViewDo(elem, state);
}

function lukToggleView(id)
{
  var elem = document.getElementById(id);
  if (elem) lukSetViewDo(elem, (elem.style.display == 'none'));
}

function lukAcknowledgeCookies()
{
  lukSetCookie('lukcookieinfo', 1, 31*24*60*60);
  var elem = document.getElementById('cookieInfo');
  if (elem) lukSetViewDo(elem, false);
}
</script>

<div id=\"devInfo\" ".($showDevInfo ? "" : " style=\"display: none;\"").">
 <h1>Notice!</h1>
 <p>
  <b>This is the development version of 'Lukkari', tentatively called \"3.0alpha\".</b>
  It is constantly changing, any features may be broken and fixed at
  rapid pace. The style/layout is also in flux, and not finished -
  there may be rendering bugs. Currently I am testing a non-table-based layout,
  although it is not certain if it will be used in 'production'.
 </p>
 <p>
  If you wish to suggest features, send e-mail to <b>ccr (at) tnsp (dot) org</b>
 </p>
 <div class=\"popupControls\">
  <button type=\"button\" onClick=\"lukSetView('devInfo', false);\">Okay</button>
 </div>
</div>
";

if ($showCookieInfo)
{
  echo
    "<div id=\"cookieInfo\">\n".
    " <p>Lukkari web-site uses <a href=\"http://en.wikipedia.org/wiki/HTTP_cookie\">HTTP cookies</a> ".
    " to store current settings (language, default class ID, etc.) ".
    " Also, Google Analytics used on this site may store information via cookies.".
    " </p>".
    " By continued use of this site, you acknowledge that you have been informed of the situation.".
    " <div class=\"popupControls\">\n".
    "  <button type=\"button\" onclick=\"lukAcknowledgeCookies()\">Okay</button>\n".
    " </div>\n".
    "</div>\n";
}


// Additional controls
echo
  " <div id=\"controls\">\n".
  "  <div id=\"infobox\">\n".
  "   <div id=\"ctitle\">".$pageName." v".$pageVersion."</div>\n";

if (!$mobileMode)
{
  echo
    "   <div id=\"csssel\">".cmQM("Style").": ";

  if (isset($pageCSSAlts))
  {
    foreach ($pageCSSAlts as $name => $id)
    {
      echo
        "<a ".(($pageCSSIndex == $id) ? "class=\"selected\" " : "").
        "href=\"".$baseURI."?css=".$id."\">".$name."</a>";
    }
  }

  echo
    "</div>\n";
}

echo "   <div id=\"clang\">";

foreach ($pageLanguages as $id)
{
  echo
    "<a ".(($pageLang == $id) ? "class=\"selected\" " : "").
    "href=\"".$baseURI."?lang=".$id."\">".$id."</a>";
}

echo
  "   </div>\n".
  "  </div>\n".
  "  <form action=\"".$baseURI."\" method=\"get\">\n".
  "   <div>\n".
  "    <select id=\"classSelect\" name=\"class\" onChange=\"this.form.submit();\">\n";

if ($classIDs !== FALSE)
{
  foreach ($classIDs as $id)
  {
    echo
      "     <option ".($classID == $id ? "selected=\"selected\" " : "").
      "value=\"".$id."\">".chentities($id)."</option>\n";
  }
}

echo 
  "    </select>\n".
  "   </div>\n".
  "   <noscript><div><input id=\"classSwitch\" class=\"submit\" type=\"submit\" value=\"".cmQM("change")."\" /></div></noscript>\n";

if (!$mobileMode)
{
  echo
    "   <div><a id=\"nextPeriod\" class=\"textctrl\" href=\"".
    $baseURI.($nextPeriod ? "" : "?next")."\" title=\"".cmQM("expl_toggle_period")."\">".
    cmQM("period_shown").": ".($nextPeriod ? cmQM("next_period") : cmQM("current_period")).
    "</a></div>\n";
  
  if ($haveData)
  {
    echo
    "   <div><a id=\"origLink\" class=\"textctrl\" href=\"".$origBaseURI.$classID.$origBaseExt."\" title=\"".cmQM("expl_link_original_data")."\">".cmQM("link_original_data")."</a></div>\n";
    //"   <div><a id=\"xml\" class=\"textctrl\" href=\"".$baseURI.$cachePath."/".$class.".xml\">XML</a></div>\n";
  }
  
//  echo "   <div><a id=\"mobileLink\" class=\"textctrl mobile\" href=\"http://tnsp.org/mluk/\" title=\"".cmQM("expl_link_mobile_version")."\">".cmQM("link_mobile_version")."</a></div>\n";
}

$currURL = $baseURI."?class=".$classID."&amp;lang=".$pageLang.
  (isset($pageCSSIndex) ? "&amp;css=".$pageCSSIndex : "").
  ($nextPeriod ? "&amp;next" : "");

echo
  "   <div><a href=\"".$currURL."\" class=\"textctrl\" title=\"".cmQM("expl_link_current_settings")."\">".cmQM("link_current_settings")."</a></div>\n".
  "   <div><button id=\"toggleDevInfo\" type=\"button\" onclick=\"lukToggleView('devInfo')\">INFO</button></div>\n".
  " </form>\n".
  "</div>\n".
  "<div id=\"header\">\n".
  "<h1>".$pageTitle."</h1>\n";

if (!$mobileMode)
{
  echo
    "<p>".join("; ", $classInfo["general"]).
    //" [".cmQM("viikossa", $classInfo["totalHours"])."]".
    "</p>\n";
}

// Show error messages
if ($errorSet)
{
  echo "<ul>\n";
  foreach ($errorMsgs as $msg)
    echo "<li>".$msg."</li>\n";
  echo "</ul>\n";
}

echo "</div>\n";

if ($haveData)
{
  echo lukPrintTimeTable($mobileMode);
}

echo
  "<div id=\"footer\">".
  cmQM("contact", "ccr @ IRCNet", "ccr (at) tnsp (dot) org").
  " / ".
  cmQM("updated", strftime("%d.%m.%Y, %H:%M", $timestamp)).
  //" <div style=\"color: red;\">".cmQM("beta", $pageVersion, $pageName)."</div>\n".
  "</div>\n";

cmPrintPageFooter();


// Dump the course data cache, but only if it has changed
if ($cacheDirty)
{
  $str = "<?\n\$cache = ".var_export($cache, TRUE)."\n?>";
  if (file_put_contents($courseCacheFile, $str, LOCK_EX) === FALSE)
  {
    // Can't do much anything here ..
  }
}

?>