A Smart Form Script

From this page you can download a smart form script, that almost needs no configuring, but still handles posted forms very securely. This form is an ideal solution if you don’t have an extended programming knowledge and yet want to have a highly configurable form. The form configuration is done by special attributes in the html of the form. The visitor of you site will not get to see these attributes.

The comments in the script will help you to understand and configure the script. All files visible below are available in an archive: zip-archive.

For your convenience the three files in this archive are listed below, with added syntax coloring. A working example of this form can be seen here. This example will not send any mails however, to prevent the sending of spam mails.

An example page

This is an example of an contact page that contains the dynamically generated form.

download
0001<?php
0002//This PHP block MUST be placed at the absolute top of the document. Make sure you haven't placed any kind of whitespace or text before it! Otherwise PHP won't be able to send the headers that are required for correct functioning of the form.
0003 
0004require_once("formchecker.php");
0005?>
0006<!DOCTYPE html PUBLIC "-//W3CDTD XHTML 1.0 TransitionalEN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
0007<html xmlns = "http://www.w3.org/1999/xhtml">
0008<head>
0009<meta http-equiv = "Content-Type" content = "text/html; charset = utf-8" />
0010<title>Formuliervalidatie</title>
0011<link href = "form.css" rel = "stylesheet" type = "text/css" />
0012</head>
0013 
0014<body>
0015<?php
0016$checker = new formchecker();
0017?>
0018<h1>Formulier</h1>
0019 
0020<form action = "" method = "post">
0021<ul>
0022  <li class = "required minlength-8 maxlength-60 name-opmerking"><label for = "opmerking">type hier een opmerking</label> *<input type = "text" id = "opmerking" name = "opmerking" value = "" /></li>
0023        <!--with the class "required" in the li you mark a field as such. This is the only special class that will not be removed by the script, so you can give required fields a special lay-out, should you wish so-->
0024        <!--with a class "type" followed by a hyphen and then the type of the field, you configure the validation of the field. For example: a field in a li with the class type-email must contain a valid e-mail address-->
0025        <!--with a class "name" followed by a hyphen and then the name of the field, you configure the name and id of the field-->
0026        <!--the class "sendconfirmationhere" marks a field that will receive the e-mail address of the visitor; confirmation mails will be sent there-->
0027  <li class = "required type-email sendconfirmationhere name-email"><label for = "email">vul een geldig e-mailadres in</label> *<input type = "text" id = "email" name = "email" value = "" /></li>
0028 
0029        <!--with a class "expected" followed by a hyphen and then the expected value of the field, you configure the required value of a field. Great for checking that a checkbox has indeed been checked ("expected-on")-->
0030  <li class = "required expected-on name-ch"><label for = "ch">vink aan</label> *<input type = "checkbox" id = "ch" name = "ch" /></li>
0031 
0032 
0033        <!--with a class "valid" followed by a hyphen and one or more names separated by hyphens you signify valid fieldnames. Great for dropdowns or radiogroups, when you want to ensure that a hacker will not be able to inject fieldnames in a form that you didn't configure-->
0034  <li class = "required valid-radio1-radio2 name-RadioGroup1">Maak een keuze
0035   <ul>
0036    <li><label for = "RadioGroup1_0">eerste keuze</label> *
0037      <input type = "radio" name = "RadioGroup1" value = "radio1" id = "RadioGroup1_0" />
0038      Radio</label></li>
0039    <li><label>tweede keuze</label> *
0040      <input type = "radio" name = "RadioGroup1" value = "radio2" id = "RadioGroup1_1" />
0041      Radio</li>
0042   </ul>
0043  </li>
0044 
0045  <li class = "required valid-een-twee name-test"><label for = "test">keuze verplicht</label> *<select id = "test" name = "test">
0046    <option value = "...">maak een keuze...</option>
0047    <option value = "een">een</option>
0048    <option value = "twee">twee</option>
0049  </select></li>
0050 
0051       <!--with a class "minlength" and/or "maxlength" followed by a hyphen and a number you configure the required minimal length and/or the allowed maximal length of the field's content-->
0052  <li class = "required minlength-8 maxlength-120 name-zo"><label for = "zo">dit veld is verplicht</label> *<textarea id = "zo" name = "zo"></textarea></li>
0053 
0054  <li id = "knoppen"><input type = "submit" value = "Verzend" name = "verzend" id = "verzend" /></li>
0055  </ul>
0056</form>
0057 
0058<!--within the element "tagBevestigingInBrowser" you can define the html that the visitor will see in his or her browser after submitting the form. The marker "( (gegevens))" will be dynamically replaced with the data the visitor posted. If the visitor has given an e-mail address, within the element tagInzenderEmailadres the marker "( (e-mailadres))" will be replaced with the e-mail address given. Otherwise the element will be removed from the html.-->
0059<tagBevestigingInBrowser>
0060<!--
0061 * Deze html wordt gebruikt voor de weergave van de bevestiging van een inzending in de browser.
0062    * Je kunt de tekst in de H1 en de P aanpassen maar let op het volgende:
0063 * Laat het element <tagInzenderEmailadres> en de code  hieronder exact zo staan: hier wordt het e-mailadres van de inzender dynamisch ingevuld, indien het is opgegeven.
0064 * Op de plek van  worden de waarden van de door de bezoeker ingevulde velden geplaatst, dus laat ook deze code ongemoeid.
0065    * Laat ook de tags <tagBevestigingInBrowser> staan, deze zijn nodig, deze verzorgen het bericht in de browser.
0066-->
0067<h1>Formulier verzonden</h1>
0068<p>Bedankt voor uw reactie! <tagInzenderEmailadres>Er is een bevestigingsmail verzonden naar .</tagInzenderEmailadres></p>
0069<p>Dit zijn de gegevens zoals u die ons toegestuurd hebt:</p>
0070 
0071
0072</tagBevestigingInBrowser>
0073 
0074<?php
0075$checker->check();
0076?>
0077 
0078</body>
0079</html>

Smart Form class

This is the class that does all the hard work of generating forms, processing posted data and securing them.

download
0001<?php
0002/*
0003CREDITS
0004 
0005* Het formulier is verzorgd door Alex Pot van www.smartscripts.nl.
0006* Het is bedoeld voor studenten en cursisten die onze lessen volgen, zodat ze zelfstandig hun eerste projecten kunnen voorzien van een degelijk en betrouwbaar formulier.
0007* Gebruik je dit formulierscript of geef je het door, laat de credits dan intact!
0008 
0009*/
0010 
0011header("Expires: ".gmdate("D, d M Y H:i:s")." GMT"); // Always expired
0012header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");// always modified
0013header("Cache-Control: no-cache, must-revalidate");// HTTP/1.1
0014header("Pragma: nocache");// HTTP/1.0
0015 
0016if (!defined("locaal"))
0017{
0018 if (isset($_SERVER, $_SERVER['SERVER_NAME']) && stristr($_SERVER['SERVER_NAME'],"localhost")) {define("locaal",true);} else {define("locaal",false);}
0019}
0020 
0021class formchecker {
0022 private $_email_admin = "dwaal@mac.com";
0023 
0024 private $_email_onderwerp_admin = "Formulier gepost op "; //code  wordt dynamisch vervangen door sitenaam.nl
0025 private $_email_onderwerp_inzender = "De gegevens van het door u op  ingezonden formulier";
0026 
0027 private $_email_inhoud = "DOOR U INGEVULDE GEGEVENS \n\n"; //wordt in het script dynamisch aangevuld. De code  wordt vervangen door de datum van inzending
0028 private $_bevestiging_in_browser = "<h2>Door u ingevulde gegevens</h2>\r\n\r\n<ul class = \"bevestiginginzending\">";
0029 
0030 //termen die duiden op acitviteiten van hackers (met | van elkaar gescheiden en binnen () blijvend)
0031 private $_verbodentermen = "/(?:\bb?cc\s*:|Content\-Type|\bfrom\s*:|\[(?:url|action|link))/i";
0032 
0033 
0034 // ======================== SCRIPT HIERONDER ONGEWIJZIGD LATEN ======================
0035 
0036 private $_velden = Array();
0037 private $_postmodus = false;
0038 private $_textareas = Array();
0039 
0040 //speciale aansturingsclasses die uit de classes van de li's verwijderd moeten worden, zodat de bezoeker ze niet te zien krijgt (class required niet verwijderd, omdat die nog van pas kan komen voor de opmaak. class "name"  hier niet noemen, omdat die voor de validatie nodig is):
0041 private $_remove_classes = Array("expected", "maxlength", "minlength", "sendconfirmationhere", "type", "valid");
0042 
0043 private $_inzender_email_veld = ""; //wordt dynamisch ingevuld, indien de inzender een adres heeft opgegeven
0044 private $_inzender_email = ""; //wordt dynamisch ingevuld met de inhoud van het vorige veld, indien de inzender een adres heeft opgegeven
0045 
0046 private $_bevestiging_in_browser_template = "";
0047 
0048 
0049 public function __construct () {
0050 
0051  require_once("simple_html_dom.php");
0052 
0053  if (isset($_POST, $_POST['verzend']))$this->_postmodus = true;
0054  ob_start();
0055 }
0056 
0057 public function check () {
0058  $formulier = ob_get_clean();
0059 
0060  //bevestingsbericht inlezen uit de html, indien het formulier werd verzonden:
0061  if ($this->_postmodus) {
0062   preg_match("/<tagBevestigingInBrowser>(.*?)<\/tagBevestigingInBrowser>/sim", $formulier, $out);
0063   if (isset($out[0]))$this->_bevestiging_in_browser_template = $out[1];
0064 
0065   //commentaar uit de html van de bevestiging verwijderen:
0066   $this->_bevestiging_in_browser_template = preg_replace("/<!\-\-.*?\-\->/sim", "", $this->_bevestiging_in_browser_template);
0067  }
0068 
0069  //bevestigingsbericht verwijderen uit de html:
0070  $formulier = preg_replace("/<tagBevestigingInBrowser>.*?<\/tagBevestigingInBrowser>/sim", "", $formulier);
0071 
0072  $formulier = str_get_html($formulier); //hier gaan we dus SimpleHtmlDom gebruiken
0073 
0074  //zoek naar verplichte velden:
0075  $verplichtevelden = $formulier->find('li[class* = required]');
0076 
0077  $foutvelden = Array();
0078 
0079  if (count($verplichtevelden)) {
0080   //DebugBreak();
0081 
0082   foreach ($verplichtevelden as $li) {
0083    $this->_get_field_properties_from($li);
0084   }
0085 
0086   //indien er niets gepost is, het formulier simpelweg echon:
0087   if (!$this->_postmodus) {
0088    $this->_remove_name_classes($formulier);
0089 
0090    $this->_whitespace_correct($formulier);
0091    echo $formulier;
0092   }
0093 
0094   else { //valideren met PHP
0095    $valide = true;
0096    foreach ($this->_velden as $name => $props) {
0097     if ($props['type'] == "email" && !$this->_validate_email($_POST[$name])) {
0098      $foutvelden[$name] = "Geen geldig e-mailadres ingevuld";
0099      $valide = false;
0100      continue;
0101     }
0102     if ($props['valid-values'] && !in_array($_POST[$name], $props['valid-values'])) {
0103      $foutvelden[$name] = "Geen geldige waarde geselecteerd";
0104      $valide = false;
0105      continue;
0106     }
0107     if ($props['expected'] && $_POST[$name] != $props['expected']) {
0108      $foutvelden[$name] = "Verwachte waarde niet ingevuld";
0109      $valide = false;
0110      continue;
0111     }
0112     if ($props['min-length'] && strlen($_POST[$name]) < $props['min-length']) {
0113      $foutvelden[$name] = "Te weinig tekens";
0114      $valide = false;
0115      continue;
0116     }
0117     if ($props['max-length'] && strlen($_POST[$name]) > $props['max-length']) {
0118      $foutvelden[$name] = "Teveel tekens";
0119      $valide = false;
0120      continue;
0121     }
0122 
0123     //controle op regeleindes in non-textarea's (dat duidt op hackers):
0124     if (!in_array($field, $this->_textareas) && preg_match("/(?:\r|\n|[%]0A)/", $_POST[$name]) && !array_key_exists($name, $foutvelden)) {
0125      $foutvelden[$name] = "Ongeldige tekens gebruikt!";
0126      $valide = false;
0127      continue;
0128     }
0129 
0130     //controle op verboden termen (dat duidt op hackers):
0131     if (preg_match($this->_verbodentermen, $_POST[$name]) && !array_key_exists($name, $foutvelden)) {
0132      $foutvelden[$name] = "Verboden termen gebruikt!";
0133      $valide = false;
0134      continue;
0135     }
0136    }
0137   }
0138 
0139   if ($this->_postmodus && !$valide)$this->_formulier_herinvullen($formulier, $foutvelden);
0140   elseif ($this->_postmodus) $this->_formulier_verzenden();
0141  }
0142 
0143  else {
0144   $this->_whitespace_correct($formulier);
0145   echo $formulier;
0146  }
0147 }
0148 
0149 private function _formulier_herinvullen ($formulier, $foutvelden = Array()) {
0150 
0151  $formulier = str_get_html($formulier);
0152 
0153  foreach ($_POST as $field => $value) {
0154   if ($this->_velden[$field]['type'] == "textarea") {
0155    $li = $formulier->find("li[class* = name-{$field}]", 0);
0156    if (array_key_exists($field, $foutvelden)) {
0157     $li->class = $li->class . " foutveld";
0158     $li->innertext = $li->innertext . "<span class = \"foutmelding\">" . $foutvelden[$field] . "</span>";
0159    }
0160    //bug: herinvullen van textarea's wordt genegeerd, dus daarom hieronder apart gedaan
0161   }
0162 
0163   elseif (in_array($this->_velden[$field]['type'], Array("text", "email", "checkbox"))) {
0164    $li = $formulier->find("li[class* = name-{$field}]", 0);
0165    if (array_key_exists($field, $foutvelden)) {
0166     $li->class = $li->class . " foutveld";
0167     $li->innertext = $li->innertext . "<span class = \"foutmelding\">" . $foutvelden[$field] . "</span>";
0168    }
0169 
0170    //herinvullen:
0171    $veld = $formulier->find("input[name = {$field}]", 0);
0172    if (in_array($this->_velden[$field]['type'], Array("text", "email")))$veld->value = $_POST[$field];
0173    else $veld->checked = "checked";
0174   }
0175 
0176   elseif ($this->_velden[$field]['type'] == "select") {
0177    $li = $formulier->find("li[class* = name-{$field}]", 0);
0178    if (array_key_exists($field, $foutvelden)) {
0179     $li->class = $li->class . " foutveld";
0180     $li->innertext = $li->innertext . "<span class = \"foutmelding\">" . $foutvelden[$field] . "</span>";
0181    }
0182    else {
0183     //herinvullen:
0184     $dropdown = $li->find("select[name = {$field}]", 0);
0185     $dropdown->innertext = preg_replace("/(value\s* = \s*([\"'])" . $_POST[$field] . "\2)/i", "\1 selected = \"selected\"", $dropdown->innertext);
0186    }
0187   }
0188   elseif ($this->_velden[$field]['type'] == "radio") {
0189    $li = $formulier->find("li[class* = name-{$field}]", 0);
0190    if (array_key_exists($field, $foutvelden)) {
0191     $li->class = $li->class . " foutveld";
0192     $li->innertext = $li->innertext . "<span class = \"foutmelding\">" . $foutvelden[$field] . "</span>";
0193    }
0194    else {
0195     //herinvullen:
0196     $li->innertext = preg_replace("/(value\s* = \s*([\"'])" . $_POST[$field] . "\2)/i", "\1 checked = \"checked\"", $li->innertext);
0197    }
0198   }
0199 
0200  } //einde for
0201 
0202 
0203  foreach ($foutvelden as $key => $melding) {
0204   if (!isset($_POST[$key])) { //bijv. checkboxen of radiobuttons worden niet gepost als ze niet aangevinkt zijn en zijn dan dus niet terug te vinden in $_POST
0205    $li = $formulier->find("li[class* = name-{$key}]", 0);
0206    $li->class = $li->class . " foutveld";
0207    $li->innertext = $li->innertext . "<span class = \"foutmelding\">" . $foutvelden[$key] . "</span>";
0208   }
0209  }
0210 
0211  $this->_remove_name_classes($formulier);
0212 
0213  $formulier = $formulier->save();
0214 
0215  foreach ($this->_textareas as $area) {
0216   $formulier = preg_replace("/(<textarea[^>]*?name\s* = \s*[\"']{$area}[\"'][^>]*?>)/i", "\1" . $_POST[$area], $formulier, 1);
0217  }
0218 
0219  $this->_whitespace_correct($formulier);
0220  echo $formulier;
0221 }
0222 
0223 private function _formulier_verzenden () {
0224  $this->_email_inhoud = str_replace("", date("d-m-Y, H:i:s"), $this->_email_inhoud);
0225 
0226  foreach ($_POST as $veld => $value) {
0227   if ($veld == "verzend")continue;
0228 
0229   //indien veld met adres van inzender aangegeven en ingevuld, dan dat adres hier inlezen. Hoeft niet meer gevalideerd te worden, want dat heeft het script al in een eerder stadium gedaan:
0230   if ($this->_inzender_email_veld != "" && $veld == $this->_inzender_email_veld) {
0231    $this->_inzender_email = $value;
0232   }
0233 
0234 
0235   $this->_email_inhoud . = "* " . $veld . ": " . $value . "\n\n";
0236   $this->_bevestiging_in_browser . = "<li> " . $veld . ": " . $value . "</li>\r\n";
0237  }
0238  $this->_bevestiging_in_browser . = "</ul>\r\n";
0239 
0240  $valid_inzender_adres = ($this->_inzender_email != "" && $this->_validate_email($this->_inzender_email));
0241 
0242  if (!locaal) {
0243   //als safe mode op server uitgeschakeld is, dan mail met fifth parameter verzenden, zodat het afzenderadres er mooier uitziet en er gereageerd kan worden op de door het script verzonden mailtjes (afzenderadres dan resp. dat van de inzender of dat van de admin)
0244   $safe_mode = strtolower(ini_get("safe_mode"));
0245   $use_fifth = (in_array($safe_mode, Array("off", "0")));
0246 
0247   $onderwerp = str_replace("", str_replace("www.", "", $_SERVER['SERVER_NAME']), $this->_email_onderwerp_admin);
0248 
0249   if (!$use_fifth || !$valid_inzender_adres) {
0250    mail($this->_email_admin, $onderwerp, $this->_email_inhoud);
0251   }
0252   else {
0253    mail($this->_email_admin, $onderwerp, $this->_email_inhoud, "From:{$this->_inzender_email}", "-f {$this->_inzender_email}");
0254   }
0255 
0256   if ($this->_inzender_email != "" && $valid_inzender_adres) {
0257    $onderwerp = str_replace("", str_replace("www.", "", $_SERVER['SERVER_NAME']), $this->_email_onderwerp_inzender);
0258    if (!$use_fifth) {
0259     mail($this->_inzender_email, $onderwerp, $this->_email_inhoud);
0260    }
0261    else {
0262     mail($this->_inzender_email, $onderwerp, $this->_email_inhoud, "From:{$this->_email_admin}", "-f {$this->_email_admin}");
0263    }
0264   }
0265  }
0266 
0267  //bevestigingsmelding in browser weergeven:
0268  if ($this->_bevestiging_in_browser_template != "") {
0269 
0270   if (!$valid_inzender_adres) {
0271    $this->_bevestiging_in_browser_template = preg_replace("/<tagInzenderEmailadres>.*?<\/tagInzenderEmailadres>/sim", "", $this->_bevestiging_in_browser_template);
0272   }
0273   else {
0274    $this->_bevestiging_in_browser_template = preg_replace("/<\/?tagInzenderEmailadres>/i", "", $this->_bevestiging_in_browser_template);
0275    $this->_bevestiging_in_browser_template = str_replace("", $this->_inzender_email, $this->_bevestiging_in_browser_template);
0276   }
0277 
0278   $this->_bevestiging_in_browser = str_replace("", $this->_bevestiging_in_browser, $this->_bevestiging_in_browser_template);
0279 
0280   echo $this->_bevestiging_in_browser;
0281  }
0282 }
0283 
0284 private function _whitespace_correct (&$formulier) {
0285  $formulier = preg_replace("/(\s+)</", "\r\n\1<", $formulier);
0286 }
0287 
0288 private function _get_field_properties_from ($li) {
0289  $class = $li->class;
0290  $li_html = $li->outertext;
0291  preg_match("/name\-([^\"' ]+)/i", $class, $name);
0292  $name = $name[1];
0293 
0294  $type = "text";
0295 
0296  if (stristr($li_html, "textarea"))$type = "textarea";
0297  elseif (stristr($li_html, "<select"))$type = "select";
0298  elseif (preg_match("/type\s* = \s*[\"'](radio|checkbox)/i", $li_html, $out)) {
0299   $type = $out[1];
0300  }
0301 
0302  if ($type == "textarea")$this->_textareas[] = $name;
0303 
0304  $this->_velden[$name] = Array("type" => $type);
0305 
0306  if (preg_match("/\btype\-([^\"' ]+)/", $class, $out)) { //bijv. type-email
0307   $this->_velden[$name]['type'] = trim($out[1]);
0308  }
0309 
0310  //evt. veld tbv e-mailadres inzender bepalen:
0311  if (preg_match("/sendconfirmationhere/", $class, $out)) {
0312   $this->_inzender_email_veld = $name;
0313  }
0314 
0315  $this->_velden[$name]['expected'] = (preg_match("/expected\-([^\"' ]+)/", $class, $out)) ? $out[1] : null;
0316  $this->_velden[$name]['min-length'] = (preg_match("/minlength\-(\d+)/", $class, $out)) ? $out[1] : null;
0317  $this->_velden[$name]['max-length'] = (preg_match("/maxlength\-(\d+)/", $class, $out)) ? $out[1] : null;
0318  $this->_velden[$name]['valid-values'] = Array();
0319  if (preg_match("/\bvalid\-([^\"' ]+)/", $class, $out)) {
0320   $this->_velden[$name]['valid-values'] = split("-", trim($out[1]));
0321  }
0322 
0323  //classes opschonen, zodat die voor de bezoeker niet zichtbaar worden en zo de werking van het formulier verraden:
0324  foreach ($this->_remove_classes as $rclass) {
0325   $class = preg_replace("/\b{$rclass}(?:\-[a-z0-9_\-]+)?/i", "", $class);
0326  }
0327  $class = trim(preg_replace("/\s{2,}/", " ", $class));
0328  $li->class = $class;
0329 }
0330 
0331 private function _remove_name_classes (&$formulier) {
0332  $lis = $formulier->find("li[class* = name-]");
0333  foreach ($lis as $li) {
0334   $li->class = preg_replace("/(\s+)?name\-[\-a-z0-9_]+/i", "", $li->class);
0335  }
0336 }
0337 
0338 private function _validate_email ($value) {
0339  return preg_match("/^[a-z.0-9_\-]+@[a-z.0-9_\-]+?\.[a-z]{2,5}$/i", $value);
0340 }
0341}

Stylesheet

This is the stylesheet that gives the form it’s lay-out. You can adapt it to your heart’s content.

download
0001li.foutveld {background: #FFE4E1; border: 1px solid #C00; padding: 5px;margin-top: 1px;}
0002li.foutveld span {display: block;}
0003 
0004form {
0005 font-weight: bold;
0006 color: #630;
0007}
0008form, fieldset, #knoppen, form ul, form li {
0009 width: 43em;
0010}
0011fieldset {
0012 font-size: 80%;
0013 padding-bottom: 1em;
0014 /* border: 0; schakelt border van de fieldset uit */
0015}
0016 
0017form ul, form li {
0018 list-style: none;
0019 margin: 0;
0020 padding: 0;
0021 overflow: hidden; /* mocht de tekst in een floating label te hoog worden, dan rekt het dankzij deze instelling de li toch nog op */
0022}
0023 /* Voeg deze regel toe om de stijl van IE (blauw) te overrulen
0024legend {
0025 color: #630;
0026} */
0027label {
0028 float: left;
0029 clear: left;
0030 color: #630;
0031 font-weight: bold;
0032 text-align: right;
0033 padding-top: .6em;
0034 width: 13em;
0035 padding-right: 1em;
0036}
0037input, textarea, select {
0038 font: 100%/150% Verdana, Geneva, sans-serif;
0039}
0040input, textarea {
0041 border: 1px solid #630;
0042 margin: .6em;
0043 background: #ECFFFF;
0044 padding: 1px; /* zo interne instellingen voor inputs van bijvoorbeeld Chrome overrulen */
0045}
0046textarea {
0047 height: 6em;
0048 width: 20em !important; /* !important: zo verhinder je dat in Chrome of Firerox het veld breder kan worden en daardoor de labeltekst van zijn plek verdringen, wanneer de bezoeker via het driehoekje rechts onderin het tekstveld groter sleept */
0049 overflow: auto; /* deze beter voor IE6 op visible zetten om onnodige scrollbalken te voorkomen */
0050}
0051#knoppen {
0052 border-width: 0px;
0053}
0054#verzendknop, #wisknop {
0055 margin-top: 1em;
0056}
0057#knoppen li {
0058 text-align: center;
0059}
0060select#kiesmaand {
0061 left: 12.0em;
0062 margin-top: 0.5em;
0063}

A Basic Script to Switch Between Mobile and Desktop Layout

On this page a basic PHP script will be presented to switch between a mobile and desktop version of a webpage. The main target for creating this script was to present a working model that can be used for educational purposes. This way it may become clear how such an approach can be realized and students can experience the main functions that we like to emphasize as important, which are:

  • it automatically detects what kind of browser a visitor uses;
  • it presents the appropriate version of the webpage for desktop and mobile;
  • the automated presented desktop and mobile versions of the webpage will use the same filename (One Web);
  • the script allows the visitor to overrule this automatically determined choice;
  • the visitor’s preference will be stored in a cookie so that at a next visit with the same browser he does not need to use the switch again;
  • since this last function cannot be done without redirection there is a canonical link to the main version of the webpage present (One Web).

A working example of this script can be found here

The code

Click on the filenames below to see the code of the corresponding file and the companion instructions.

.htaccess

The .htaccess file needs the following code to set the cache-control for the stylesheets. Since cache-control for PHP needs to be set with PHP you will not find the cache-control settings for the webpages in the .htacces file below.

In case you don’t have access to put a .htacces file on your server, you will find special stylesheets below that contain the cache-control with PHP.

download
0001<ifModule mod_headers.c>
0002 <FilesMatch ".(css)$">
0003 Header set Expires "Thu, 15 Apr 2040 20:00:00 GMT"
0004 Header set Content-Type "text/css; charset = utf-8"
0005 Header set Cache-Control "max-age = 1100"
0006 </FilesMatch>
0007</ifModule>

page.php

This webpage is a template for all the pages in the site. There is no need to bother about the canonical link, the script will automatically take care of it, same as for any duplicate page. The cookie will be automatically placed by the script as well, for all versions of the web page, desktop and mobile and duplicates.

The PHP code is kept out of the HTML as much as possible without losing the aim to make clear what needs to be done and how this can be done. This means there is some PHP in the webpages that can’t be left out:

  • See the comments inside the code of the webpage, don’t leave out to read them. The comments at line 9 are especially important and are about the PHP variable at line 10.
  • You can edit the value of the lifetime of the cookie in the PHP code at line 11. The companion comments explain how.
  • The switch for mobile/desktop looks like this:
    <?php echoSwitch('Desktop Page', 'Mobile Page'); ?>
    The content for either desktop or mobile can be placed between the single quotation marks. Here the content on desktop will be ‘Desktop Page’ and on mobile it will be ‘Mobile Page’. You can place this PHP code where ever you like in the content of the webpage, just make sure you paste it from the first < up to the close >. Any content outside of this code will be shown on both desktop and mobile.
  • At line 12 you will find the $maxage variable so that you can set the cache-control for each page as you like
  • At line 34 another PHP code begins that switches desktop/mobile content. If you look closely you will see that the single quotation marks as you can see them in the sample just mentioned, are missing inside the PHP code at line 34 and are replaced with HEREDOC notation. Read the comments that start at line 54 to learn more about this.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009//if your files are in the rootfolder of your site, $subdir has no value written between the quotation marks, like this ''; else you name the subdir, like so: '/namesubdir' (note it has a slash at the start, BUT NOT AT THE END!). This sample suggests to name the folder 'switch', this is the value used below. This is correct as long as the folder is placed in the root and not in a subfolder; else the value should be '/namesubfolder/switch'.
0010$subdir = '/switch'; //change this according to the name of the folder your files are in (see also instruction above)
0011$cookie_lifetime = 60*60*24*30; //you may change the numbers: sec x min x hrs x days = how long (in seconds) must the cookie with the user preference persist?
0012$maxage = 60; //this is the value (in seconds) for the max-age header that the switcher-script sends. This time will determine how long the version op the page in your browser cache will stay unrefreshed. Note: changes in the user preference for the lay-out will only become definitive after this number of seconds.
0013 
0014//==================== DON'T CHANGE PHP VARIABLES BELOW THIS LINE ==========================
0015 
0016$root = $_SERVER['DOCUMENT_ROOT'] . $subdir;
0017$file = __FILE__;
0018 
0019//include required by the mobile-desktop switch script:
0020include($root . '/inc/switcher.php');
0021?>
0022<!DOCTYPE html PUBLIC "-//W3CDTD XHTML Basic 1.1EN"
0023  "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
0024<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en">
0025<head>
0026  <title><?php echoSwitch('Desktop Page', 'Mobile Page'); ?></title>
0027  <link rel = "canonical" href = "<?php echo $full_path; ?>" />
0028  <link rel = "stylesheet" type = "text/css" href = "<?php echoSwitch('screen_styles', 'mobile_styles'); ?>.css" />
0029</head>
0030<body>
0031<p id = "mob_switch"><a href = "<?php echo $current_file['basename']; ?>?m = <?php echoSwitch('1', '0'); ?>"><?php echoSwitch('Mobile View', 'Desktop View'); ?></a> (changes will become definitive after a delay of <?php echo $maxage; ?> seconds or a reload of the page)</p>
0032<h1><?php echoSwitch('Desktop', 'Mobile'); ?> Presentation</h1>
0033<p>This static page represents the <?php echoSwitch('desktop', 'mobile'); ?> presentation of the resource available at <?php echo $full_path; ?>.</p>
0034<?php
0035echoSwitch(
0036<<<DESKTOP
0037 
0038<h2>Content with quotation marks</h2>
0039<p id = "sample">In 'reprehenderit' in voluptate lorem ipsum dolor sit amet: "consectetur adipisicing elit". Ut aliquip ex ea commodo consequat. Sed do eiusmod tempor incididunt cupidatat non proident, in reprehenderit in voluptate. Ut enim ad minim veniam, ullamco laboris nisi mollit anim id est laborum.</p>
0040<p>Eu fugiat nulla pariatur. Duis aute irure dolor lorem ipsum dolor sit amet, sunt in culpa. Velit esse cillum dolore excepteur sint occaecat qui officia deserunt. Eu fugiat nulla pariatur. Duis aute irure dolor in reprehenderit in voluptate ullamco laboris nisi.</p>
0041<p>Ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, in reprehenderit in voluptate excepteur sint occaecat. Ut aliquip ex ea commodo consequat. Duis aute irure dolor ut enim ad minim veniam, ullamco laboris nisi.</p>
0042 
0043DESKTOP
0044,
0045<<<MOBILE
0046 
0047<h2>Content with quotation marks</h2>
0048<p id = "sample">In 'reprehenderit' in voluptate lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut aliquip ex ea commodo consequat. Sed do eiusmod tempor incididunt cupidatat non proident, in reprehenderit in voluptate. Ut enim ad minim veniam, ullamco laboris nisi mollit anim id est laborum.</p>
0049 
0050MOBILE
0051); ?>
0052<p><a href = "page2.php">PAGE 2</a></p>
0053 
0054<!-- Read this comment about placing content with quotation marks:
0055 
0056Take a look above at for instance in line 26 where is written 'Desktop Page' and 'Mobile Page'. The text Desktop Page will be presented on desktop and the text Mobile Page on mobile. If you replace that text with a larger block of content, then that is the content that wil be presented instead. If that contant contains single quotation marks, you are in trouble: the browser will read these as PHP instead of the HTML you mean it to be.
0057 
0058If content has been placed via the PHP HEREDOC-notation (see the section above with the header 'Content with quotation marks'), then that way quotation marks in the content won't pose a problem anymore. As you can see in that section, that does contain these single quotation marks.
0059 
0060The HEREDOC-notation is done by replacing the start quotation marks with what is written instead at line 36 and at the end at line 43.
0061 
0062It is not impossible to place content in PHP code that uses single quotation marks like in the sample at line 26. All you need to do is place one backslash before any single quotation mark inside the content like this Tim\'s. This will make the browser present the content correctly. It may be clear that using HEREDOC-notation instead for larger pieces of content, is safer.
0063 
0064Note that PHP may use double quotation marks instead of single ones. In that case an HTML attribute like an ID or a class may cause similar problems. You can solve this with a backslash before the double quotaton marks like this id = \"test\".
0065 
0066By the way: you may use the same HEREDOC notations as many times as you like in one page. So DESKTOP and MOBILE as markers don't need to be unique. You may also use just D instead of DESKTOP and just M instead of MOBILE, or another variation, that's up to you, just keep the syntax similar.
0067 
0068End of comment -->
0069 
0070</body>
0071</html>

page2.php

This page is created by duplicating the page above. Just make sure that any new webpage that you create will be a duplicate of that page as well and you will be fine. Don’t forget to fill in the page title for desktop and mobile. There is no need to bother about the canonical link, the script will take care of it. Also the cookie will be placed by the script.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009//if your files are in the rootfolder of your site, $subdir has no value written between the quotation marks, like this ''; else you name the subdir, like so: '/namesubdir' (note it has a slash at the start, BUT NOT AT THE END!). This sample suggests to name the folder 'switch', this is the value used below. This is correct as long as the folder is placed in the root and not in a subfolder; else the value should be '/namesubfolder/switch'.
0010$subdir = '/switch'; //change this according to the name of the folder your files are in (see also instruction above)
0011$cookie_lifetime = 60*60*24*30; //you may change the numbers: sec x min x hrs x days = how long (in seconds) must the cookie with the user preference persist?
0012$maxage = 60; //this is the value (in seconds) for the max-age header that the switcher-script sends. This time will determine how long the version op the page in your browser cache will stay unrefreshed. Note: changes in the user preference for the lay-out will only become definitive after this number of seconds.
0013 
0014//==================== DON'T CHANGE PHP VARIABLES BELOW THIS LINE ==========================
0015 
0016$root = $_SERVER['DOCUMENT_ROOT'] . $subdir;
0017$file = __FILE__;
0018 
0019//include required by the mobile-desktop switch script:
0020include($root . '/inc/switcher.php');
0021?>
0022<!DOCTYPE html PUBLIC "-//W3CDTD XHTML Basic 1.1EN"
0023  "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
0024<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en">
0025<head>
0026  <title><?php echoSwitch('Desktop Page 2', 'Mobile Page 2'); ?></title>
0027  <link rel = "canonical" href = "<?php echo $full_path; ?>" />
0028  <link rel = "stylesheet" type = "text/css" href = "<?php echoSwitch('screen_styles', 'mobile_styles'); ?>.css" />
0029</head>
0030<body>
0031<p id = "mob_switch"><a href = "<?php echo $current_file['basename']; ?>?m = <?php echoSwitch('1', '0'); ?>"><?php echoSwitch('Mobile View', 'Desktop View'); ?></a> (changes will become definitive after a delay of <?php echo $maxage; ?> seconds or a reload of the page)</p>
0032<h1><?php echoSwitch('Desktop', 'Mobile'); ?> Presentation of page 2</h1>
0033<p>This static page represents the <?php echoSwitch('desktop', 'mobile'); ?> presentation of the resource available at <?php echo $full_path; ?>.</p>
0034<?php
0035echoSwitch(
0036<<<DESKTOP
0037 
0038<h2>Lorem Ipsum</h2>
0039<p>Eu fugiat nulla 'pariatur'. Ut aliquip ex ea commodo consequat. Ullamco laboris nisi sunt in culpa qui officia deserunt. Excepteur sint occaecat cupidatat non proident, ut enim ad minim veniam. In reprehenderit in voluptate sunt in culpa velit esse cillum dolore. Mollit anim id est laborum. Quis nostrud exercitation ut enim ad minim veniam, ut aliquip ex ea commodo consequat. Excepteur sint occaecat ut labore et dolore magna aliqua. Ut enim ad minim veniam, duis aute irure dolor velit esse cillum dolore. Ullamco laboris nisi ut labore et dolore magna aliqua. Excepteur sint occaecat sunt in culpa eu fugiat nulla pariatur. Ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, ut labore et dolore magna aliqua. Sed do eiusmod tempor incididunt excepteur sint occaecat lorem ipsum dolor sit amet. Consectetur adipisicing elit, in reprehenderit in voluptate sunt in culpa. Sed do eiusmod tempor incididunt ullamco laboris nisi ut enim ad minim veniam.</p>
0040 
0041DESKTOP
0042,
0043<<<MOBILE
0044 
0045<h2>Lorem Ipsum</h2>
0046<p>Eu fugiat nulla 'pariatur'. Ut aliquip ex ea commodo consequat. Ullamco laboris nisi sunt in culpa qui officia deserunt. Excepteur sint occaecat cupidatat non proident, ut enim ad minim veniam. In reprehenderit in voluptate sunt in culpa velit esse cillum dolore. Mollit anim id est laborum. Quis nostrud exercitation ut enim ad minim veniam, ut aliquip ex ea commodo consequat.</p>
0047 
0048MOBILE
0049); ?>
0050<p><a href = "page.php">HOME</a></p>
0051</body>
0052</html>

screen_styles.css

Write your CSS for the desktop version of your webpages in this stylesheet. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. If your server does not allow you to put an .htaccess file, you need to use the stylesheet below, screen_styles.php, instead. Read more about this at ‘Download the files’.

download
0001body {font-family:sans-serif; color:black; background-color:#ccf}

screen_styles.php

This stylesheet is meant for those who cannot put an .htaccess file on their server. Write your CSS for the desktop version of your webpages in this stylesheet below the PHP code. Leave this PHP code in top of the stylesheet untouched, it contains the Cache-Control header instead of writing it in .htaccess. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. See below at ‘Download the files’ for additional instructions.

download
0001<?php
0002header("Content-Type: text/css; charset = utf-8");
0003header("Cache-Control: max-age = 36000");
0004?>
0005 
0006body {font-family:sans-serif; color:black; background-color:#ccf}

mobile_styles.css

Write your CSS for the mobile version of your webpages in this stylesheet. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. If your server does not allow you to put an .htaccess file, you need to use the stylesheet below, mobile_styles.php, instead. Read more about this at ‘Download the files’.

download
0001body {font-family:sans-serif; color:#ccf; background-color:black}
0002a {color:red}

mobile_styles.php

This stylesheet is meant for those who cannot put an .htaccess file on their server. Write your CSS for the mobile version of your webpages in this stylesheet below the PHP code. Leave this PHP code in top of the stylesheet untouched, it contains the Cache-Control header instead of writing it in .htaccess. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. See below at ‘Download the files’ for additional instructions.

download
0001<?php
0002header("Content-Type: text/css; charset = utf-8");
0003header("Cache-Control: max-age = 36000");
0004?>
0005body {font-family:sans-serif; color:#ccf; background-color:black}
0006a {color:red}

inc/switcher.php

This script is developed in teamwork with and for the Mobile Web and Apps Best Practices Training of W3C Online Training.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009if (!isset($root)) {
0010 header("HTTP/1.0 404 Not Found"); //hide existence of this file
0011 die;
0012}
0013 
0014header("Vary: User-Agent, Accept");
0015 
0016//construct the base url (including the directory, but not the file name):
0017$base = "http://" . $_SERVER['SERVER_NAME'] . preg_replace("/[^\/]*(\?.*)?$/", "", $_SERVER['REQUEST_URI']);
0018 
0019$mobile_browser = 0;
0020$display_mode_changed = false;
0021 
0022if (isset($_GET['m']) && in_array($_GET['m'], Array("0", "1"))) { // We have a value directly from the user that we need to store
0023  setcookie('m', $_GET['m'], time()+$cookie_lifetime);	// Although we may already have a cookie, the value may
0024  $_COOKIE['m'] = $_GET['m'];						// have changed so we'll store it anyway. Also update $_COOKIE array.
0025 
0026  if ($_GET['m'] == "1")$mobile_browser++;
0027 
0028  $display_mode_changed = true;
0029}
0030 
0031elseif (isset($_COOKIE['m']) && $_COOKIE['m'] == "1") {// If we have a cookie set to 1 or if we have
0032  $mobile_browser++;								// just set it to 1, we want the mobile view
0033}
0034 
0035elseif (isset($_COOKIE['m']) && $_COOKIE['m'] == "0") { //forced Desktop View
0036  $mobile_browser = 0;
0037}
0038 
0039else {						// No indication of user preference
0040  include($root . "/inc/mobile_detection_class.php");	// include the detector script
0041  $detect = new Mobile_Detect();
0042  $mobile_browser = $detect->isMobile(); //returns 0 or 1
0043}		//OK, we're done. We know which version we want so let's return it
0044 
0045 
0046if (!$display_mode_changed)header("Cache-Control: max-age = " . $maxage);				// Set cache control before we go any further
0047else {
0048 //force a quicker refresh of (only) the current page after the visitor has changed the display mode manually:
0049 header("Expires: ".gmdate("D, d M Y H:i:s")." GMT"); // Always expired
0050 header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");// always modified
0051 header("Cache-Control: no-cache, must-revalidate, max-age = 0");// HTTP/1.1
0052 header("Pragma: nocache");// HTTP/1.0
0053 header("Cache-Control: max-age = 0"); //force refresh of this page
0054}
0055 
0056$current_file = pathinfo($file);
0057$full_path = $base . $current_file['basename'];
0058 
0059function echoDesk ($text) {
0060 global $mobile_browser;
0061 if ($mobile_browser == 0 ) {
0062  echo $text;
0063 }
0064}
0065 
0066function echoMobile ($text) {
0067 global $mobile_browser;
0068 if ($mobile_browser > 0 ) {
0069  echo $text;
0070 }
0071}
0072 
0073function echoSwitch ($desktoptext, $mobiletext) {
0074 global $mobile_browser;
0075 if ($mobile_browser > 0 ) {
0076  echo $mobiletext;
0077 }
0078 else echo $desktoptext;
0079}

inc/mobile_detection_class.php

The lightweight class for detection of mobile browsers device detection script below can also be found at http://www.opensource.org

download
0001<?php
0002 
0003/**
0004 * Mobile Detect
0005 *
0006 * @license    http://www.opensource.org/licenses/mit-license.php The MIT License
0007 * @version    SVN: $Id: Mobile_Detect.php,v 1.1 2011/07/06 14:40:34 phila Exp $
0008 */
0009 
0010class Mobile_Detect {
0011 
0012 protected $accept;
0013 protected $userAgent;
0014 
0015 protected $isMobile     = false;
0016 protected $isAndroid    = null;
0017 protected $isBlackberry = null;
0018 protected $isIphone = null;
0019 protected $isOpera      = null;
0020 protected $isPalm       = null;
0021 protected $isWindows    = null;
0022 protected $isGeneric    = null;
0023 
0024 protected $devices = array(
0025  "android"       => "android",
0026  "blackberry"    => "blackberry",
0027  "iphone"        => "(iphone|ipod)",
0028  "opera"         => "opera mini",
0029  "palm"          => "(avantgo|blazer|elaine|hiptop|palm|plucker|xiino)",
0030  "windows"       => "windows ce; (iemobile|ppc|smartphone)",
0031  "generic"       => "(kindle|mobile|mmp|midp|o2|pda|pocket|psp|symbian|smartphone|treo|up.browser|up.link|vodafone|wap)"
0032 );
0033 
0034 
0035 public function __construct () {
0036  $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
0037  $this->accept    = $_SERVER['HTTP_ACCEPT'];
0038 
0039  if (isset($_SERVER['HTTP_X_WAP_PROFILE'])|| isset($_SERVER['HTTP_PROFILE'])) {
0040   $this->isMobile = true;
0041  } elseif (strpos($this->accept,'text/vnd.wap.wml') > 0 || strpos($this->accept,'application/vnd.wap.xhtml+xml') > 0) {
0042   $this->isMobile = true;
0043  } else {
0044   foreach ($this->devices as $device => $regexp) {
0045    if ($this->isDevice($device)) {
0046     $this->isMobile = true;
0047    }
0048   }
0049  }
0050 }
0051 
0052 
0053 /**
0054  * Overloads isAndroid() | isBlackberry() | isOpera() | isPalm() | isWindows() | isGeneric() through isDevice()
0055  *
0056  * @param string $name
0057  * @param array $arguments
0058  * @return bool
0059  */
0060 public function __call ($name, $arguments) {
0061  $device = substr($name, 2);
0062  if ($name == "is" . ucfirst($device)) {
0063   return $this->isDevice($device);
0064  } else {
0065   trigger_error("Method $name not defined", E_USER_ERROR);
0066  }
0067 }
0068 
0069 
0070 /**
0071  * Returns true if any type of mobile device detected, including special ones
0072  * @return bool
0073  */
0074 public function isMobile () {
0075  return ($this->isMobile) ? 1 : 0;
0076 }
0077 
0078 
0079 protected function isDevice ($device) {
0080  $var    = "is" . ucfirst($device);
0081  $return = $this->$var === null ? (bool) preg_match("/" . $this->devices[$device] . "/i", $this->userAgent) : $this->$var;
0082 
0083  if ($device != 'generic' && $return == true) {
0084   $this->isGeneric = false;
0085  }
0086 
0087  return $return;
0088 }
0089}

Download the files

All files mentioned above are available in an archive: zip-archive.

About the PHP

According the PHP there are two things worth mentioning:

  • For the use of the script your server needs to support PHP 5.
  • To keep the script out of the HTML as much as possible for this basic method, you will find the main part of the script in the folder named ‘inc’, where it is split up in two parts: one is the device detection and the other one is responsible for the switch between presentation of the mobile or desktop version of the webpage.

Version with .htaccess

For those among you that have the rights to put a .htaccess on the server, you need to do the following:

  • Write your CSS in the files named desktop_styles.css and mobile_styles.css.
  • You can delete the files named desktop_styles.php and mobile_styles.php

Version without .htaccess

For those among you that are not allowed to work with .htaccess on the server, you need to do the following:

  • Change the link in the web page(s) to the stylesheets from .css to .php
  • Write your CSS in the files named desktop_styles.php and mobile_styles.php. Be careful to leave the PHP code in the beginning of the stylesheets untouched.
  • You can delete the files named desktop_styles.css and mobile_styles.css
  • You can delete the .htaccess file

To complete the download there is a readme file included.

Force update of AppCache with PHP

AppCache can be hard to deal with. Browsers seem unpredictable in updating the AppCache. This example of a PHP-script (for PHP 5) makes sure the AppCache gets updated when pages have been changed.

Important

This script relies heavily on http-headers sent dynamically by PHP. Headers that are defined in .htaccess can never be overruled by PHP, so don’t put these headers (especially Last-Modified and Expires) in .htaccess: the script won’t work then.

The Process: two modi

The script will be executed on the server in two modi: from the manifest and from a regular webpage on your site.

To clarify this let’s name the first mode “manifest-mode” and the second mode “page-mode”. Both modi use different parts of the PHP-script in manifest-loader.php.

  • In manifest-mode the script will be activated directly by .htaccess. In this mode the script will echo the contents of manifest.appcache to the browser, but not before it has searched for pages that are more recent than the manifest, in all the folders that are listed in a special file named folders.txt. Since almost every page of your site may link to the manifest, every time one of these pages is visited, the script will start its search for modificated pages.
  • In page-mode if the script is included in the page, it will only check if the current webpage is more recent than the manifest. In page-mode the script sends cache-control-headers (Cache-Control: max-age, Last-Modified, Expires) for the current webpage to the browser.

More about the manifest-mode

  1. The script will investigate if the script itself or one of your regular webpages is more recent than the manifest.
  2. If so, the manifest on the server will be updated with an new version-number and modification-time. Then it will be sent to the browser, along with some headers that tell the browser that the manifest has expired and will have to be refreshed from the browser during the next request of a webpage.
  3. The update of the online manifest forces the visitor’s browser to update its local AppCache.
  4. In case there are no webpages more recent than the manifest, the script checks if maybe your stylesheet is more recent than the manifest. In that case the manifest also will be updated.
  5. There may be a delay before you see updates of the manifest. This delay is determined by:
    1. The number of files in the appcache that have to be checked for updates (and the time it takes to download modificated files).
    2. On the max-age send sent by the script for each file while in page-mode.

More about the page mode

  1. If the requested webpage on the server is more recent than the manifest, the script will update the manifest on the server with a new version-number and modification-time.
  2. The script also checks if the requested page is in a folder that is not yet listed in folders.txt. If so, this file will be updated with the new folder. This list of folders enables the script to check this new folder for modificated pages when it’s in manifest-mode.

The Files

You need four files, each of which you can find below:

  1. The manifest that is called manifest.appcache.
  2. The .htaccess file
  3. The script that is called manifest-loader.php
  4. A register for the folders on your site folders.txt

Also you need to include some extra PHP-code in your regular webpages, as you can see in the samples below.

All files mentioned above are available in an archive: zip-archive.

Don’t forget to make manifest.appcache and folders.txt writable! Without this permission the script will not be able to write/change/add anything in these files.

The Manifest

This is an example of the manifest thats is needed to enable AppCache. Modify according to your requirements, but don’t change the first two occurrences of lines that start with “#”.

download
0001CACHE MANIFEST
0002 
0003# Don't touch the following two lines; they will be updated dynamically by manifest-loader.php:
0004# Version: 58
0005# Updated: 2011-09-05, 16:26:57
0006 
0007CACHE:
0008 
0009/page.php
0010/css/css.css
0011 
0012#If you want to use fallback pages for pages that haven't been downloaded yet of if you want to define pages that have to be always fetched online, remove the corresponding hashtags below
0013 
0014#FALLBACK:
0015#/ /offline.php
0016 
0017#NETWORK:
0018#*

.htaccess

Only the bare minimum of rules needed for this setup are demonstrated here. This file must reside in the root directory of your site.

download
0001RewriteEngine On
0002 
0003#send the manifest:
0004rewriteRule ^manifest\.appcache$ appcache/manifest-loader.php?sendmanifest = true [L]
0005 
0006<IfModule mod_headers.c>
0007#make sure (with Etag and max-age) that modified static content will be reloaded after the manifest has been updated:
0008 <FilesMatch ".(html|htm|xml|txt|css)$">
0009  FileETag MTime Size
0010 
0011  Header set Cache-Control "max-age = 120"
0012 </FilesMatch>
0013 
0014</IfModule>

folders.txt

In this file manifest-loader.php maintains a list of folders on your site. This list enables manifest-loader.php while in manifest-mode to crawl the folders of your site in search of files that are more recent than the manifest.

download
0001a:1:{i:0;s:1:"/";}

manifest-loader.php

This is the script that does all the hard work. It has to be included in the head of your pages (see the sample below).

download
0001<?php
0002class AppCache {
0003 
0004 //first we define some properties of the class:
0005 
0006/** root folder of your site; dynamically filled in the constructor
0007* @var string */
0008 private $_root_folder = "";
0009 
0010/** The folder that contains this script and the manifest; dynamically filled in the constructor
0011* @var string */
0012 private $_appcache_folder = "";
0013 
0014/** Path to the manifest; dynamically filled in the constructor
0015* @var string */
0016 private $_manifest = "";
0017 
0018/** Contents of the manifest, fetched from manifest.appcache and sent to the browser
0019* @var string */
0020 private $_manifest_contents = "";
0021 
0022/** Will be true if and when the manifest source-file manifest.appcache has been updated. This update is executed by AppCache::_manifest_update()
0023* @var boolean */
0024 private $_manifest_updated = false;
0025 
0026/** The delay in seconds it will take before you will see updated content in your browser. 2 Minutes (120 seconds) is a good value for this: you don't have to wait overly long for updated content and your pages will still load very fast.
0027* @var numeric */
0028 private $_appcache_refresh_delay = 120;
0029 
0030/** Folders that will be checked by the dynamic manifest. Folder list will be stored in the file folders.txt in the folder appcache and will be automatically updated. Initially only the root folder of the site - "/" - is stored
0031* @var array */
0032 private $_folders = Array("/");
0033 
0034/** txt-file that stores the serialized list op folders of your site
0035* @var string */
0036 private $_folder_index = "";
0037 
0038/** Folders that contain the stylesheet(s) of your site. Has a stylesheet been updated, the manifest will also be updated, to force an update of the AppCache in the browser
0039* @var array */
0040 private $_css_folders = Array("/css/");
0041 
0042/** File modification time of the manifest, needed for computations to determine if the manifest needs an update
0043* @var numeric */
0044 private $_manifest_filemtime = 0;
0045 
0046/** True if this file has been referenced from .htaccess (when the manifest has to be echoed). If the current class has been instantiated from an include on a page, this property will stay false.
0047* @var boolean */
0048 private $_is_manifest = false;
0049 
0050/**
0051 * Constructor of the class. It will echo the manifest or updated it, if required
0052 *
0053 * @return    void
0054 */
0055 public function __construct () {
0056 
0057  //determine if this file has been loaded as stand-alone (to echo the manifest), of as an include on a page:
0058  if (isset($_GET, $_GET['sendmanifest']))$this->_is_manifest = true;
0059 
0060  //initialise the locations of the folders:
0061  $this->_root_folder = $_SERVER['DOCUMENT_ROOT'];
0062  $this->_appcache_folder = $this->_root_folder . "/appcache";
0063 
0064  $this->_folder_index = $this->_appcache_folder . "/folders.txt";
0065 
0066  //initialize some properties:
0067  $this->_folders = unserialize(file_get_contents($this->_folder_index));
0068 
0069  $this->_manifest = $this->_appcache_folder . "/manifest.appcache";
0070 
0071  $this->_manifest_filemtime = filemtime($this->_manifest);
0072 
0073  //if in manifest mode, echo the contents of manifest.appcache:
0074  if ($this->_is_manifest)$this->_echo_manifest();
0075 
0076  //otherwise: if this file has been loaded from a regular php-page:
0077  else {
0078   //determine the file modification time of the page that has called this file/class:
0079   $pagetime = filemtime(CALLER);
0080 
0081   //if the page is newer then the manifest, update the manifest:
0082   if ($pagetime > $this->_manifest_filemtime)$this->_manifest_update();
0083 
0084   //send some headers for cache-control of the page (don't use the second argument true for header(), or you'll get internal server errors):
0085   header("Cache-Control: max-age = " . $this->_appcache_refresh_delay);
0086   header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT", $pagetime);
0087 
0088   $expires = time() + 60 * 60 * 24 * 14;
0089   header("Expires: ".gmdate("D, d M Y H:i:s", $expires)." GMT"); // Far future expiration header set to 14 days
0090 
0091   //optionally update the list of folders that have to be checked for changes in the php-pages (list will be stored in appcache/folders.txt):
0092   $this->_update_folders();
0093  }
0094 
0095 } //end of __construct()
0096 
0097/**
0098 * Echoes the manifest and before that conditionally updates the manifest if pages or resources of the site have been updated
0099 *
0100 * @return    void
0101 */
0102 private function _echo_manifest () {
0103 
0104  // if this class / file has been updated, update the manifest:
0105  if (filemtime(__FILE__) > $this->_manifest_filemtime)$this->_manifest_update();
0106 
0107  //if the manifest hasn't been updated by the previous line, check the folders of the site for changes in de php-pages.
0108  //if any are found, the manifest will then be also updated, in order to force a refresh of the local AppCache of the browser
0109  if (!$this->_manifest_updated) {
0110   //will contain folders that are listed in folders.txt but don't exist anymore:
0111   $delete_folders = Array();
0112 
0113   foreach ($this->_folders as $folder) {
0114    $folder_to_check = $this->_root_folder . substr($folder, 0, -1);
0115 
0116    //if a folder doesn't exist anymore, add it to a list. The folders in this list will be purged from folders.txt later on:
0117    if (!file_exists($folder_to_check) || !is_dir($folder_to_check)) {
0118     $delete_folders[] = $folder;
0119     continue;
0120    }
0121 
0122    //determine the most recent file modification time in a folder:
0123    $most_recent_page = $this->_get_most_recent_file_in_folder($folder_to_check);
0124 
0125    //if the most recent page in a folder is more recent than the manifest, update the manifest:
0126    if ($most_recent_page > $this->_manifest_filemtime) {
0127     $this->_manifest_update();
0128 
0129     break; //because the manifest has been updated, the folders don't have to be checked anymore for files with more recent modification times
0130    }
0131   }
0132 
0133   //if there are any folders listed in folders.txt that don't exist anymore, delete them from this file:
0134   if (count($delete_folders)) {
0135    $this->_folders = array_diff($this->_folders, $delete_folders);
0136    file_put_contents($this->_folder_index, serialize($this->_folders));
0137   }
0138  }
0139 
0140 
0141  // only if an update of the manifest has not been neccessary until now, check the shylesheets for modification. If a stylesheet exists that is more recent than the manifest, update the manifest:
0142  if (!$this->_manifest_updated) {
0143   foreach ($this->_css_folders as $folder) {
0144    $folder_to_check = $this->_root_folder . substr($folder, 0, -1);
0145 
0146    $most_recent_page = $this->_get_most_recent_file_in_folder($folder_to_check, Array("extension" => "css"));
0147 
0148    if ($most_recent_page > $this->_manifest_filemtime) {
0149     $this->_manifest_update();
0150 
0151     break; //because the manifest has been updated, the other folders don't have to be checked anymore for files with more recent modification times
0152    }
0153   }
0154  }
0155 
0156  //send some headers for the manifest. Specifically needed for FireFox, which browser has a tendency to cache the manifest and therefor not to update the AppCache. With these headers the manifest will be marked as always expired, even for Firefox:
0157 
0158  header("Content-Type: text/cache-manifest", true);
0159  header("Cache-Control: must-revalidate, proxy-revalidate, max-age = 0", true);
0160  header("Expires: ".gmdate("D, d M Y H:i:s")." GMT", true); // Always expired
0161  header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT", true);// always modified
0162 
0163 
0164  //echo the contents of the sourcefile manifest.appcache:
0165  readfile($this->_manifest);
0166 
0167 } //end of _echo_manifest()
0168 
0169 
0170/**
0171 * Update the manifest, to force an update of the AppCache in the browser
0172 *
0173 * @return    void
0174 */
0175 private function _manifest_update () {
0176 
0177  $this->_manifest_contents = file_get_contents($this->_manifest);
0178 
0179  $this->_manifest_contents = preg_replace_callback("/Version:\s*(\d+)/", create_function(
0180    '$var',
0181    '
0182    $var[1] += 1;
0183    return "Version: "  . $var[1];
0184    '
0185  ), $this->_manifest_contents);
0186 
0187  $this->_manifest_contents = preg_replace("/# Updated: .+/", "# Updated: " . date("Y-m-d, H:i:s"), $this->_manifest_contents);
0188 
0189  file_put_contents($this->_manifest, $this->_manifest_contents);
0190 
0191  //log the modified-status of the manifest; this way further checks for the need to update the manifest will be skipped
0192  $this->_manifest_updated = true;
0193 
0194 } //end of _manifest_update()
0195 
0196/**
0197 * If an update thereof is required, update the list of folders with pages in folders.txt
0198 *
0199 * @return   void
0200 */
0201 private function _update_folders () {
0202 
0203  //determine the path to files relative to the document root of the site:
0204  $folder = preg_replace("/[^\/]+$/", "", $_SERVER['REQUEST_URI']);
0205 
0206  //if "http" has been used in the request_uri, a hacker probably is trying things. So we won't continue with the method in that case.
0207  if (stristr($folder, "http"))return;
0208 
0209  //you have to make sure the folder index file folders.txt exists AND it has to be writable.
0210  // if a new folder has been detected that isn't in this list, it has to be added to the index file:
0211  if (!in_array($folder, $this->_folders)) {
0212   $this->_folders[] = $folder;
0213   file_put_contents($this->_folder_index, serialize($this->_folders));
0214  }
0215 }
0216 
0217/**
0218 * Returns the most recent file in a folder
0219 *
0220 * @param string $folder The folder that is being searched
0221 * @param array $options Options for the method
0222 *
0223 * @return    numeric file modification time
0224 */
0225 private function _get_most_recent_file_in_folder ($folder, $options = Array()) {
0226 
0227  if(!file_exists($folder) || !is_dir($folder))return "";
0228 
0229  extract($options);
0230 
0231  if (!isset($extension))$extension = "php";
0232 
0233  $list = scandir ($folder); //scandir() returns a list of files (only the filenames) in the folder
0234  if (count($list) == 2)return ""; // then only .. and . found, which are no regular files
0235 
0236  $most_recent_filemtime = 0;
0237 
0238  foreach ($list as $file) {
0239 
0240   $full_path = $folder . "/" . $file;
0241 
0242   if (in_array($file, Array(".", "..")) || is_dir($full_path) || ($extension != "" && !preg_match("/\." . $extension . "$/", $file)))continue;
0243 
0244   $time = filemtime($full_path);
0245   if ($time > $most_recent_filemtime) {
0246    $most_recent_filemtime = $time;
0247   }
0248  } //end of the loop through all files
0249 
0250  return $most_recent_filemtime;
0251 
0252 } //end of get_most_recent_file_in_folder()
0253 
0254} //end of class AppCache
0255 
0256//instantiate the class, so that the manifest will be echoed or updated:
0257new AppCache();

Sample files

Below you find examples of three regular pages (two in the root folder of your site and one in a subfolder “subdir”) and of the stylesheet for these pages.

Notice how the manifest loader is being loaded in the start section of your pages. You only have to add some extra PHP-code above the doctype and in the HTML element of your pages. Of course your webpages must have the extension .php for this to work.

In the root folder: page.php (in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 1</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 1</h1>
0017<p>This is the only page that is explicitly listed in the manifest. The other pages will be added to the AppCache when the visitor loads them in his browser.</p>
0018<p>To <a href = "page2.php">page 2</a></p>
0019<p>To <a href = "subdir/page3.php">page 3</a></p>
0020</body>
0021</html>

In the root folder: page2.php (not in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 2</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 2</h1>
0017<p>This page is not listed in the manifest; so it will only be added to the AppCache if and when the visitor has visited this page in his browser.</p>
0018<p>To <a href = "page.php">page 1</a></p>
0019<p>To <a href = "subdir/page3.php">page 3</a></p>
0020</body>
0021</html>

In a subfolder: page3.php (not in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 3</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 3 (in subfolder)</h1>
0017<p>This page is not listed in the manifest; so it will only be added to the AppCache if and when the visitor has visited this page in his browser.</p>
0018<p>To <a href = "../page.php">page 1</a></p>
0019<p>To <a href = "../page2.php">page 2</a></p>
0020</body>
0021</html>

The stylesheet: css/css.css (in the manifest)

The contents of this file are not very relevant. I’ve only added it here because this sheets also will be checked for modifications by manifest-loader.php.

download
0001body {
0002 background: red;
0003 color: white;
0004}
0005 
0006a {
0007 color: white;
0008 text-decoration: underline;
0009}
0010 
0011a:hover {
0012 text-decoration: none;
0013}