Mise en cache des pages de WikiNi
Une brève discussion avait été proposée quant à la mise en cache des pages dans les
SuggestionsRapiditeDeTraitement. Cette page est destinée à en discuter de manière plus détaillée, et surtout à en évaluer les performances.
NB.: cette page ne concerne en rien la mise en cache des requêtes sql.
Le système de cache que je propose:
J'ai impléménté (en fait c'est surtout recopier le
FormatterWakka? et le remagnier un peu) un formatter
cache.php qui permet de formatter une page normalement sauf qu'il retourne le code php à sauver dans un fichier afin d'utiliser ce dernier directement plutôt que de formatter la page à chaque nouvel affichage. Le gain se fait uniquement côté php car la requête de lecture de la page dans la base de données se fait de façon automatisée bien avant d'avoir sélectionné le handler, or dans le cas présent, seul le
HandlerShow est concerné. (cf
ClasseWikiMethodeRun?)
Fonctionnement
- Il faut spécifier dans wakka.config.php le répertoire où les pages devront être mises en cache (et s'assurer que php a les droits en écriture dessus). Pour désactiver la cache, il suffit de laisser cette valeur vide: 'pagecache_dir' => 'cache/pages/' (le slash '/' à la fin est très important !)
- Lors de la demande d'affichage d'une page, le handler show va tenter d'inclure (via IncludeBuffered) la version en cache de la page (si la mise en cache est activée). S'il n'y arriver pas, il va mettre en cache la page en question, et de nouveau l'inclure. Les fichiers seront sauvés sous la forme "numero_de_version.php" (ceci peut d'ailleurs permettre d'obtenir facilement le numéro de fichier via (int) $nomDuFichier et pourrait servir pour la purge), ce qui permet de mettre en cache indiféremment n'importe quelle version des pages, et de ne pas avoir à se soucier de supprimmer les fichiers en cache lors de l'édition.
- La "fabrication" du fichier à mettre en cache se fait via le formatter cache.php. Celui-ci va construire le contenu à sauver dans un fichier, en utilisant du html pour tout ce qui est statique (typiquement tout ce qui concerne l'apparence de la page ainsi que le texte, les liens statiques etc.) et du php pour tout ce qui est variable (actions, inclusions raw, liens internes, liens interwiki [pour le cas où on modifierait interwiki.conf], les liens forcés [a priori on ne sait pas de quel type de lien il s'agit...]). Il va aussi ajouter en début de contenu la vérification habituelle de la sécurité sur l'existance de la constante 'WIKINI_VERSION'. Comme il s'agit d'un module, seul le HandlerShow sera affecté (car on le modifiera en conséquence)
Todo:
- purger les pages en cache lorsque l'on purge la base de données.NB.: on n'est pas obligé de conserver de vieilles versions en cache vu que celles-ci ne sont de toute façon dans la base de données et qu'elles ne sont en général pas consultées fréquemment. On pourrait donc ajouter une valeur de configuration supplémentaire de X jours pour la suppressions des pages en cache [7 jours devrait être sufisant si on conserve toujours la dernière version] et modifier le HandlerShow de sorte qu'il ne remette pas en cache les pages ayant plus de X jours à moins qu'il s'agisse de la dernière version.
- Fichiers concernés: wakka.php, handlers/page/show.php
- supprimmer les pages en cache lorsque l'on supprime une page de la base
- FIchier concerné: wakka.php (DeleteOrphanedPage: une requête de sélection simple préalable à la suppression)
- Permettre éventuellement de régénérer facilement les pages en cache (par exemple en cas de mise à jour de WikiNi)
- ... ?
Changelog
- 2004-01-08: correction d'une importante faille de sécurité: le php et l'asp pouvaient être interprétés dans les pages en cache... (y a-t-il d'autres langages susceptibles de l'être ?)
- Ceci me fait d'ailleurs me poser la question de savoir si ne serait pas mieux que le code résultant du formatter cache soit composé d'une suite d'appels à 'echo'... ce serait probablement plus lent mais tout de même plus sécuritaire...
Sources:
formatters/cache.php (je ne sais pas si je peux le passer sous GPL...)
<?php
/**
* Fichier cache.php: formatter permettant de construire un fichier php
* (basé sur le formatter wakka.php)
* @version $Idv 0.0.1 2005-01-08 $
* @copyright 2002, Hendrik Mans <hendrik@mans.de>
* @copyright 2002, 2003 David DELON
* @copyright 2002, 2003 Charles NEPOTE
* @copyright 2002, 2003 Patrick PAUL
* @copyright 2003 Eric DELORD
* @copyright 2003 Eric FELDSTEIN
* @copyright 2004 Jean Christophe ANDRÉ
* @copyright 2005 Didier LOISEAU
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// This may look a bit strange, but all possible formatting tags have to be in a single regular expression for this to work correctly. Yup!
if (!function_exists("CacheCallback"))
{
function CacheCallback($things)
{
$thing = $things[1];
$result = '';
static $oldIndentLevel = 0;
static $oldIndentLength = 0;
static $indentClosers = array();
static $newIndentSpace = array();
static $br = 1;
global $wiki;
switch ($thing)
{
// convert HTML thingies
case '<':
return '<';
case '>':
return '>';
// styles
// bold
case '**':
static $bold = 0;
return (++$bold % 2 ? '<strong>' : '</strong>');
// italic
case '//':
static $italic = 0;
return (++$italic % 2 ? '<i>' : '</i>');
// underlinue
case '__':
static $underline = 0;
return (++$underline % 2 ? '<u>' : '</u>');
// monospace
case '##':
static $monospace = 0;
return (++$monospace % 2 ? '<tt>' : '</tt>');
// Deleted
case '@@':
static $deleted = 0;
return (++$deleted % 2 ? '<span class="del">' : '</span>');
// Inserted
case '££':
static $inserted = 0;
return (++$inserted % 2 ? '<span class="add">' : '</span>');
// header level 5
case '==':
static $l5 = 0;
$br = 0;
return (++$l5 % 2 ? "<h5>" : "</h5>\n");
// header level 4
case '===':
static $l4 = 0;
$br = 0;
return (++$l4 % 2 ? "<h4>" : "</h4>\n");
// header level 3
case '====':
static $l3 = 0;
$br = 0;
return (++$l3 % 2 ? "<h3>" : "</h3>\n");
// header level 2
case '=====':
static $l2 = 0;
$br = 0;
return (++$l2 % 2 ? "<h2>" : "</h2>\n");
// header level 1
case '======':
static $l1 = 0;
$br = 0;
return (++$l1 % 2 ? "<h1>" : "</h1>\n");
// forced line breaks
case '---':
return "<br />\n";
default:
// urls
if (preg_match("/^([a-z]+:\/\/\S+?)([^[:alnum:]^\/])?$/", $thing, $matches))
{
// Retrieve url and transform it into valid HTML (htmlentities)
$url = htmlentities ($matches[1]);
if (!isset($matches[2])) $matches[2] = '';
return "<a href=\"$url\">$url</a>" . $matches[2];
}
// escaped text
elseif (preg_match("/^\"\"(.*)\"\"$/s", $thing, $matches))
{
return str_replace(array('<?', '?>', '<%', '%>'), array('<?', '?>', '<%', '%>'), $matches[1]);
}
// code text
elseif (preg_match("/^\%\%(.*)\%\%$/s", $thing, $matches))
{
// check if a language has been specified
$code = $matches[1];
if (preg_match("/^\((.*)\)(.*)$/s", $code, $matches))
{
list(, $language, $code) = $matches;
}
else
{
$language = '';
}
// Select formatter for syntaxe hightlighting
if (file_exists("formatters/coloration_" . $language . ".php"))
{
$formatter = "coloration_" . $language;
}
else
{
$formatter = "code";
}
$output = "<div class=\"code\">";
$output .= $wiki->Format(trim($code), $formatter);
$output .= "</div>\n";
return $output;
}
// raw inclusion from another wiki
// (regexp documentation : see "forced link" below)
elseif (preg_match("/^\[\[\|(\S*)(\s+(.+))?\]\]$/", $thing, $matches))
{
list (, $url, , $text) = $matches;
if (!$text) $text = "404";
if ($url)
{
$url .= "/wakka.php?wiki=" . $text . "/raw";
return '<?php echo $this->Format($this->Format(\'' . addslashes($url) . '\', "raw"), "wakka"); ?>';
}
else
{
return "";
}
}
// forced links
// \S : any character that is not a whitespace character
// \s : any whitespace character
elseif (preg_match("/^\[\[(\S*)(\s+(.+))?\]\]$/", $thing, $matches))
{
if (isset($matches[2]) && isset($matches[3]))
{
list (, $url, , $text) = $matches;
}
else
{
list (, $url) = $matches;
}
if ($url)
{
if ($url != ($url = (preg_replace("/@@|££|\[\[/", "", $url))))$result = "</span>";
if (!isset($text)) $text = $url;
$text = preg_replace("/@@|££|\[\[/", "", $text);
return $result . '<?php echo $this->Link(\'' . addslashes($url) . '\', "", \'' . addcslashes($text, '\\\'') . '\'); ?>';
}
else
{
return "";
}
}
// indented text
elseif ((preg_match("/\n(\t+|([ ]{1})+)(-|([[:alnum:]]+)\))?/s", $thing, $matches)) || (preg_match("/^(\t+|([ ]{1})+)(-|([[:alnum:]]+)\))?/s", $thing, $matches) && $brf = 1))
{
// new line
if (isset($brf)) $br = 0;
$result .= ($br ? "<br />\n" : "");
// we definitely want no line break in this one.
$br = 0;
// find out which indent type we want
if (isset($matches[3]))
{
$newIndentType = $matches[3];
}
else
{
$newIndentType = '';
}
if (!$newIndentType)
{
$opener = "<div class=\"indent\">";
$closer = "</div>";
$br = 1;
}
elseif ($newIndentType == "-")
{
$opener = "<ul>\n";
$closer = "</li>\n</ul>\n";
$li = 1;
}
else
{
$opener = "<ol type=\"" . $matches[4] . "\">\n";
$closer = "</li>\n</ol>\n";
$li = 1;
}
// get new indent level
if (strpos($matches[1], "\t")) $newIndentLevel = strlen($matches[1]);
else
{
$newIndentLevel = $oldIndentLevel;
$newIndentLength = strlen($matches[1]);
if ($newIndentLength > $oldIndentLength)
{
$newIndentLevel++;
$newIndentSpace[$newIndentLength] = $newIndentLevel;
}
elseif ($newIndentLength < $oldIndentLength)
$newIndentLevel = $newIndentSpace[$newIndentLength];
}
$op = 0;
if ($newIndentLevel > $oldIndentLevel)
{
for ($i = 0; $i < $newIndentLevel - $oldIndentLevel; $i++)
{
$result .= $opener;
$op = 1;
array_push($indentClosers, $closer);
}
}
elseif ($newIndentLevel < $oldIndentLevel)
{
for ($i = 0; $i < $oldIndentLevel - $newIndentLevel; $i++)
{
$op = 1;
$result .= array_pop($indentClosers);
if (isset($oldIndentLevel) && isset($li)) $result .= "</li>\n";
}
}
if (isset($li) && $op) $result .= "<li>";
elseif (isset($li))
$result .= "</li>\n<li>";
$oldIndentLevel = $newIndentLevel;
$oldIndentLength = $newIndentLength;
return $result;
}
// new lines
elseif ($thing == "\n")
{
// if we got here, there was no tab in the next line; this means that we can close all open indents.
$c = count($indentClosers);
for ($i = 0; $i < $c; $i++)
{
$result .= array_pop($indentClosers);
$br = 0;
}
$oldIndentLevel = 0;
$oldIndentLength = 0;
$newIndentSpace = array();
$result .= ($br ? "<br />\n" : "\n");
$br = 1;
return $result;
}
// events
elseif (preg_match("/^\{\{(.*?)\}\}$/s", $thing, $matches))
{
if ($matches[1])
return '<?php echo $this->Action(\'' . addcslashes($matches[1], '\\\'') . '\'); ?>';
else
return "{{}}";
}
// interwiki links!
elseif (preg_match("/^" . WN_UPPER . WN_CHAR . "+[:](" . WN_CHAR2 . "*)$/s", $thing))
{
return '<?php echo $this->Link(\'' . addslashes($thing) . '\'); ?>';
}
// wiki links!
elseif (preg_match("/^" . WN_UPPER . WN_LOWER . "+" . WN_UPPER_NUM . WN_CHAR . "*$/s", $thing))
{
return '<?php echo $this->Link(\'' . addslashes($thing) . '\'); ?>';
}
// separators
else if (preg_match("/-{4,}/", $thing, $matches))
{
// TODO: This could probably be improved for situations where someone puts text on the same line as a separator.
// Which is a stupid thing to do anyway! HAW HAW! Ahem.
$br = 0;
return "<hr />\n";
}
// if we reach this point, it must have been an accident.
return $thing;
}
}
}
$text = str_replace("\r", "", $text);
$text = chop($text) . "\n";
$text = preg_replace_callback("/(\%\%.*?\%\%|" . "\"\".*?\"\"|" . "\[\[.*?\]\]|" . "\b[[:lower:]]+:\/\/\S+|" . "\*\*|\#\#|@@|££|__|<|>|\/\/|" . "======|=====|====|===|==|" . "-{4,}|---|" . "\n(\t+|([ ]{1})+)(-|[[:alnum:]]+\))?|" . "^(\t+|([ ]{1})+)(-|[[:alnum:]]+\))?|" . "\{\{.*?\}\}|" . "\b" . WN_UPPER . WN_CHAR . "+[:](" . WN_CHAR2 . "*)\b|" . "\b(" . WN_UPPER . WN_LOWER . "+" . WN_UPPER_NUM . WN_CHAR . "*)\b|" . "\n)/ms", "CacheCallback", $text);
// we're cutting the last <br />
$text = preg_replace('/<br \/>$/', '', trim($text));
echo "<?php\nif (!defined('WIKINI_VERSION'))\n\texit('Accès direct interdit');\n?>$text";
?>
Modification à apporter au
HandlerShow: remplacer la partie "display page" par:
<?php
// display page
// si la cache est activée, on l'utilise:
if ($cachedir = $this->GetConfigValue('pagecache_dir'))
{
// si IncludeBuffered ne retourne pas false, il a su lire la page en cache
if (false !== $page = $this->IncludeBuffered($file = $this->page['id'] . '.php', false, null, $cachedir))
{
echo $page;
}
// sinon cela signifie que la page n'était pas encore en cache, il faut donc l'y mettre
else
{
$fp = fopen($cachedir . $file, 'w+');
fputs($fp, $this->Format($this->page['body'], 'cache'));
fclose($fp);
// inutile de reformatter la page maintenant qu'on a une version disponible en cache...
include $cachedir . $file;
}
}
// la cache est désactivée
else echo $this->Format($this->page["body"], "wakka");
?>
Et voilà, n'oubliez pas d'ajouter la variable de configuration "pagecache_dir" et d'autoriser le répertoire en question en écriture et le tour est joué ;-) Il ne reste qu'à évaluer le gain de performances...
Exemple de version en cache d'une page: l'
ActionBackLinks:
<?php
if (!defined('WIKINI_VERSION'))
exit('Accès direct interdit');
?>Action permettant d'insérer la liste de toutes les pages faisant référence à la page courante. Dans cette présente page, {{backlinks}} donne ceci :<br />
<br />
<?php echo $this->Action('BackLinks'); ?><br />
<br />
Sur la page personnelle d'un utilisateur, cette action affichera aussi les pages dont il est le propriétaire ou le dernier modificateur.<br />
<br />
<h2> Paramètres </h2>
<br />
<h3> Paramètre "page" </h3>
Le paramètre "page" (<?php echo $this->Link('http://www.wikini.net', "", 'WikiNi'); ?> >= 0.4.1) permet de spécifier une page différente de la page courante.<br />
Par exemple <tt>{{backlinks page="PagePrincipale"}}</tt><br />
Ce paramètre peut être utile, par exemple :<br />
<ul>
<li> pour améliorer le <?php echo $this->Link('TableauDeBordDeCeWiki'); ?> : il suffit d'ajouter la liste des pages liées à la page AFaire pour connaître les pages qui doivent faire l'objet d'un travail</li>
<li> pour consolider des données sur une seule page : par exemple la liste des pages liées aux pages EstUnHomme et EstUneFemme</li>
<li> autres ?</li>
</ul>
<br />
<h3> Paramètre "exclude" </h3>
Le paramètre "exclude" (<?php echo $this->Link('http://www.wikini.net', "", 'WikiNi'); ?> >= 0.4.1) permet de spécifier des pages à exclure de la liste des pages qui ont pourtant un lien vers la page de référence.<br />
Il est en effet parfois génant d'afficher la totalité des pages faisant référence à une page. Par exemple, la page AFaire liste la page CharlesNepoteVeilleSurInternet alors que, non seulement cette information n'a pas d'intérêt mais elle pollue en outre la lecture de cette page. Autre exemple, une page MamiFeres a intérêt par exemple à lister CheVal et ElePhant mais pas nécessairement AniMaux qui est une catégorie supérieure. Je suggère donc la création d'un paramètre exclude destiné à exclure certaines pages : par exemple :<br />
<tt>{{backlinks exclude="AniMaux"}}</tt><br />
Le paramètre "exclude" peut contenir plusieurs noms de page séparés par des ";", par exemple : <tt>{{backlinks exclude="AniMaux; PagePrincipale"}}</tt><br />
<br />
<hr />
<?php echo $this->Action('trail toc="ListeDesActionsWikini"'); ?>
--
LordFarquaad