source: spip-zone/_plugins_/memoization/trunk/public/cacher.php @ 111634

Last change on this file since 111634 was 111634, checked in by cedric@…, 6 months ago

Fix un gros probleme de confidentialite avec apc/apcu :

apc/apcu fournissent le moyen de lister toutes les cles en cache. Sur un serveur qui heberge plusieurs sites cela permet donc d'acceder au cache des autres sites et d'en lire le contenu, ce qui peut etre problematique. Pour eviter cela on introduit une cle de cache propre a chaque site avec laquelle les donnes sont encodees a la mise en cache et decodees a la lecture. Impact perf absolu acceptable meme si relatif non negligeable : sur une config de test la lecture passe de 0.4µs a 1.7µs et l'ecriture de 0.8µs a 2.2µs

+ simplifcation de la purge sans foreach sur apc/apcu
+ les cles de memoization du cache SPIP sont prefixees par un 'cache:' pour permettre de les reconnaitre et distinguer vis a vis d'autres utilisations de memoization

La mise a jour vers cette version du plugin redefinit un nouveau namespace et donc invalide le cache existant, meme si on utilise pas apc ou apcu (c'est pas genial mais c'est le mieux qu'on puisse faire pour ne pas provoquer de bug chez les utilisateurs d'apc/apcu)

File size: 14.8 KB
Line 
1<?php
2
3/***************************************************************************\
4 *  SPIP, Systeme de publication pour l'internet                           *
5 *                                                                         *
6 *  Copyright (c) 2001-2018                                                *
7 *  Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James  *
8 *                                                                         *
9 *  Ce programme est un logiciel libre distribue sous licence GNU/GPL.     *
10 *  Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne.   *
11\***************************************************************************/
12
13if (!defined('_ECRIRE_INC_VERSION')) return;
14
15// par defaut pas de progressivite d'invalidation du cache : duree=0
16if (!defined("_DUREE_INVALIDATION_PROGRESSIVE_CACHE"))
17        define("_DUREE_INVALIDATION_PROGRESSIVE_CACHE",0);
18
19
20// https://code.spip.net/@generer_nom_fichier_cache
21function generer_nom_fichier_cache($contexte, $page) {
22        // l'indicateur sert a savoir un peu de quoi il s'agit
23        // quand on regarde dans le cache ; on le met a la fin
24        // du nom pour que ca "melange" mieux sous memcache
25        $indicateur = is_array($page)
26                ? $page['contexte_implicite']['cache'] # SPIP 2.1
27                : strval($page); # SPIP 2.0 ou autre
28
29        return
30                'cache:'
31                . md5(var_export(array($contexte, $page),true).'-'.$GLOBALS['dossier_squelettes'].'-'.(isset($GLOBALS['marqueur'])?$GLOBALS['marqueur']:''))
32                . '-' . $indicateur;
33}
34
35// Parano : on signe le cache, afin d'interdire un hack d'injection
36// dans notre memcache
37function cache_signature(&$page) {
38        if (!isset($GLOBALS['meta']['cache_signature'])){
39                include_spip('inc/acces');
40                include_spip('auth/sha256.inc');
41                $sigfunc = function_exists('_nano_sha256') ? '_nano_sha256' : 'md5';
42                ecrire_meta('cache_signature', $sigfunc($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SERVER_SIGNATURE"] . creer_uniqid()), 'non');
43        }
44        return crc32($GLOBALS['meta']['cache_signature'].$page['texte']);
45}
46
47/**
48 * gestion des delais d'expiration du cache...
49 * $page passee par reference pour accelerer
50 *
51 * La fonction retourne
52 * 1 si il faut mettre le cache a jour
53 * 0 si le cache est valide
54 * -1 si il faut calculer sans stocker en cache
55 *
56 * @param array $page
57 * @param int $date
58 * @return int -1|0|1
59 */
60/// https://code.spip.net/@cache_valide
61function cache_valide(&$page, $date) {
62        $now = $_SERVER['REQUEST_TIME'];
63
64        // Apparition d'un nouvel article post-date ?
65        if ($GLOBALS['meta']['post_dates'] == 'non'
66          AND isset($GLOBALS['meta']['date_prochain_postdate'])
67          AND $now > $GLOBALS['meta']['date_prochain_postdate']) {
68                spip_log('Un article post-date invalide le cache');
69                include_spip('inc/rubriques');
70                calculer_prochain_postdate(true);
71        }
72
73        if (defined('_VAR_NOCACHE') AND _VAR_NOCACHE) return -1;
74        if (isset($GLOBALS['meta']['cache_inhib']) AND $_SERVER['REQUEST_TIME'] AND $_SERVER['REQUEST_TIME']<$GLOBALS['meta']['cache_inhib']) return -1;
75        if (isset($GLOBALS['var_nocache']) AND $GLOBALS['var_nocache']) return -1;
76        if (defined('_NO_CACHE')) return (_NO_CACHE==0 AND !isset($page['texte']))?1:_NO_CACHE;
77
78        // pas de cache ? on le met a jour, sauf pour les bots (on leur calcule la page sans mise en cache)
79        if (!$page OR !isset($page['texte']) OR !isset($page['entetes']['X-Spip-Cache'])) return _IS_BOT?-1:1;
80
81        // controle de la signature
82        if ($page['sig'] !== cache_signature($page))
83                return _IS_BOT?-1:1;
84
85        // #CACHE{n,statique} => on n'invalide pas avec derniere_modif
86        // cf. ecrire/public/balises.php, balise_CACHE_dist()
87        if (!isset($page['entetes']['X-Spip-Statique']) OR $page['entetes']['X-Spip-Statique'] !== 'oui') {
88
89                // Cache invalide par la meta 'derniere_modif'
90                // sauf pour les bots, qui utilisent toujours le cache
91                if (!_IS_BOT
92                  AND $GLOBALS['derniere_modif_invalide']
93                  AND $date < $GLOBALS['meta']['derniere_modif']){
94                        // pour les admins on invalide tout de suite pour leur permettre de voir les modifs
95                        if (isset($GLOBALS['visiteur_session']['statut'])
96                          AND $GLOBALS['visiteur_session']['statut']=='0minirezo')
97                                return 1;
98
99                        // pour les autres on invalide progressivement pour repartir la charge
100                        // avec une fonction de probabilite lineaire qui vaut
101                        // 5% quand t=derniere_modif
102                        // 100% quand t=derniere_modif+_DUREE_INVALIDATION_PROGRESSIVE_CACHE
103                        static $refresh_ok = null;
104                        if (is_null($refresh_ok)){
105                                $dt = $_SERVER['REQUEST_TIME']-$GLOBALS['meta']['derniere_modif'];
106                                if ($dt>=_DUREE_INVALIDATION_PROGRESSIVE_CACHE){
107                                        $refresh_ok = 1;
108                                        #spip_log("Cache refresh systematique : REFRESH", "dbgcache");
109                                }
110                                else {
111                                        if (_DUREE_INVALIDATION_PROGRESSIVE_CACHE){
112                                                $coeff = (1-$dt/_DUREE_INVALIDATION_PROGRESSIVE_CACHE);
113                                                $seuil = 15; // 15% de probabilite au depart
114                                                $prob = mt_rand(1, $seuil+(100-$seuil)*$coeff);
115                                                $refresh_ok = ($prob<$seuil ? 1 : 0);
116                                                #spip_log("Cache refresh progresif dt=$dt coeff=$coeff p=$prob" . ($refresh_ok ? " : REFRESH" : ""), "dbgcache");
117                                        }
118                                        else {
119                                                $refresh_ok = 1;
120                                                # spip_log("Cache refresh PAS progresif REQUEST_TIME=".$_SERVER['REQUEST_TIME'].", derniere_modif=".$GLOBALS['meta']['derniere_modif']." dt=$dt", "erreur_memoization");
121                                        };
122                                }
123                        }
124
125                        // si pas de refresh force on laisse la main a la comparaison de date
126                        // selon duree de cache
127                        if ($refresh_ok)
128                                return 1;
129                }
130
131        }
132
133        // Sinon comparer l'age du fichier a sa duree de cache
134        $duree = intval($page['entetes']['X-Spip-Cache']);
135        $cache_mark = (isset($GLOBALS['meta']['cache_mark'])?$GLOBALS['meta']['cache_mark']:0);
136        if ($duree == 0)  #CACHE{0}
137                return -1;
138        // sauf pour les bots, qui utilisent toujours le cache
139        else if ((!_IS_BOT AND $date + $duree < $now)
140                # le cache est anterieur a la derniere purge : l'ignorer, meme pour les bots
141          OR $date<$cache_mark) {
142                if (_IS_BOT) return -1;
143               
144                // si la charge est trop elevee on accepte de prendre un vieux cache
145                $load = function_exists('sys_getloadavg') ? sys_getloadavg() : array(0);
146                if ($load[0]>20) {
147                        spip_log('load eleve ('. intval($load[0]).'), utilisation du cache pour '.var_export($page['source'],true).' sur '.self(), 'debug');
148                        return 0;
149                }
150
151                // sinon on calcule
152                return 1;
153        } else {
154                return 0;
155        }
156}
157
158// Creer le fichier cache
159# Passage par reference de $page par souci d'economie
160// https://code.spip.net/@creer_cache
161function creer_cache(&$page, &$chemin_cache, &$memo) {
162
163        // Ne rien faire si on est en preview, debug, ou si une erreur
164        // grave s'est presentee (compilation du squelette, MySQL, etc)
165        // le cas var_nocache ne devrait jamais arriver ici (securite)
166        // le cas spip_interdire_cache correspond a une ereur SQL grave non anticipable
167        if ((defined('_VAR_NOCACHE') AND _VAR_NOCACHE)
168                OR (isset($GLOBALS['var_nocache']) AND $GLOBALS['var_nocache']) // compat SPIP 2.x
169                OR defined('spip_interdire_cache'))
170                return;
171
172        // Si la page c1234 a un invalideur de session 'zz', sauver dans
173        // 'tmp/cache/MD5(chemin_cache)_zz'
174        if (isset($page['invalideurs'])
175        AND isset($page['invalideurs']['session'])) {
176                // on verifie que le contenu du chemin cache indique seulement
177                // "cache sessionne" ; sa date indique la date de validite
178                // des caches sessionnes
179                if (!is_array($tmp = $memo->get($chemin_cache))) {
180                        $tmp = array(
181                                'invalideurs' => array('session' => ''),
182                                'lastmodified' => $_SERVER['REQUEST_TIME']
183                        );
184                        $ok = $memo->set($chemin_cache, $tmp);
185                        spip_log((_IS_BOT?"Bot:":"")."Creation du cache sessionne $chemin_cache ". $memo->methode ." pour "
186                                . $page['entetes']['X-Spip-Cache']." secondes". ($ok?'':' (erreur!)'));
187                }
188                $chemin_cache .= '_'.$page['invalideurs']['session'];
189        }
190
191        // ajouter la date de production dans le cache lui meme
192        // (qui contient deja sa duree de validite)
193        $page['lastmodified'] = $_SERVER['REQUEST_TIME'];
194
195        // signer le contenu
196        $page['sig']= cache_signature($page);
197
198        // compresser si elle est > 16 ko
199        if (strlen($page['texte']) > 16384
200        AND function_exists('gzcompress')) {
201                $page['texte'] = gzcompress($z = $page['texte']);
202                $page['gz'] = true;
203        }
204
205        // memoizer...
206        // on ajoute une heure histoire de pouvoir tourner
207        // sur le cache quand la base de donnees est plantee (a tester)
208        $ok = $memo->set($chemin_cache, $page, 3600+$page['entetes']['X-Spip-Cache']);
209
210        // retablir le texte pour l'afficher
211        if (isset($z)) {
212                $page['texte'] = $z;
213                unset($page['gz']);
214        }
215
216        spip_log((_IS_BOT?"Bot:":"")."Creation du cache $chemin_cache ". $memo->methode ." pour "
217                . $page['entetes']['X-Spip-Cache']." secondes". ($ok?'':' (erreur!)'));
218
219        // Inserer ses invalideurs
220        /* compat SPIP 1.9 : ne pas appeler les invalideurs du tout */
221        if (!(isset($GLOBALS['spip_version']) AND $GLOBALS['spip_version']<2)) {
222                include_spip('inc/invalideur');
223                maj_invalideurs($chemin_cache, $page);
224        }
225}
226
227
228// purger un petit cache (tidy ou recherche) qui ne doit pas contenir de
229// vieux fichiers ; (cette fonction ne sert que dans des plugins obsoletes)
230// https://code.spip.net/@nettoyer_petit_cache
231function nettoyer_petit_cache($prefix, $duree = 300) {
232        // determiner le repertoire a purger : 'tmp/CACHE/rech/'
233        $dircache = sous_repertoire(_DIR_CACHE,$prefix);
234        if (spip_touch($dircache.'purger_'.$prefix, $duree, true)) {
235                foreach (preg_files($dircache,'[.]txt$') as $f) {
236                        if ($_SERVER['REQUEST_TIME'] - (@file_exists($f)?@filemtime($f):0) > $duree)
237                                spip_unlink($f);
238                }
239        }
240}
241
242
243// Interface du gestionnaire de cache
244// Si son 3e argument est non vide, elle passe la main a creer_cache
245// Sinon, elle recoit un contexte (ou le construit a partir de REQUEST_URI)
246// et affecte les 4 autres parametres recus par reference:
247// - use_cache qui vaut
248//     -1 s'il faut calculer la page sans la mettre en cache
249//      0 si on peut utiliser un cache existant
250//      1 s'il faut calculer la page et la mettre en cache
251// - chemin_cache qui est le chemin d'acces au fichier ou vide si pas cachable
252// - page qui est le tableau decrivant la page, si le cache la contenait
253// - lastmodified qui vaut la date de derniere modif du fichier.
254// Elle retourne '' si tout va bien
255// un message d'erreur si le calcul de la page est totalement impossible
256
257// https://code.spip.net/@public_cacher_dist
258function public_cacher($contexte, &$use_cache, &$chemin_cache, &$page, &$lastmodified) {
259        $chemin_cache_session = false;
260       
261        /* compat SPIP 1.9 */
262        if (is_null($contexte) AND function_exists('nettoyer_uri'))
263                $contexte = array('uri' => nettoyer_uri());
264
265        static $memo;
266        if (!isset($memo)) {
267                // cas d'un appel depuis un fichier options charge avant celui de memoization
268                if (!class_exists("MCache")){
269                        include_spip("memoization_options");
270                }
271                $cfg = @unserialize($GLOBALS['meta']['memoization']);
272                $memo = new MCache((isset($cfg['pages']) AND $cfg['pages'])? $cfg['pages'] : $cfg['methode']);
273        }
274
275        /* compat SPIP 1.9 */
276        if (is_array($page) AND !isset($page['entetes']['X-Spip-Cache']))
277                $page['duree'] = $page['entetes']['X-Spip-Cache'] = isset($GLOBALS['delais']) ? $GLOBALS['delais'] : null;
278
279        // Second appel, destine a l'enregistrement du cache sur le disque
280        if (isset($chemin_cache)) return creer_cache($page, $chemin_cache, $memo);
281
282        // Toute la suite correspond au premier appel
283        $contexte_implicite = $page['contexte_implicite'];
284
285        // Cas ignorant le cache car completement dynamique
286        if ($_SERVER['REQUEST_METHOD'] == 'POST'
287        OR (substr($contexte_implicite['cache'],0,8)=='modeles/') 
288        OR (_request('connect'))
289// Mode auteur authentifie appelant de ecrire/ : il ne faut rien lire du cache
290// et n'y ecrire que la compilation des squelettes (pas les pages produites)
291// car les references aux repertoires ne sont pas relatifs a l'espace public
292        OR test_espace_prive()) {
293                $use_cache = -1;
294                $lastmodified = 0;
295                $chemin_cache = "";
296                $page = array();
297                return;
298        }
299
300        // Controler l'existence d'un cache nous correspondant
301        $chemin_cache = generer_nom_fichier_cache($contexte, $page);
302        $lastmodified = 0;
303
304        // charger le cache s'il existe
305        if (!is_array($page = $memo->get($chemin_cache)))
306                $page = array();
307
308        // s'il est sessionne, charger celui correspondant a notre session
309        if (isset($page['invalideurs'])
310        AND isset($page['invalideurs']['session'])) {
311                $chemin_cache_session = $chemin_cache . '_' . spip_session();
312                if (is_array($page_session = $memo->get($chemin_cache_session))
313                AND $page_session['lastmodified'] >= $page['lastmodified'])
314                        $page = $page_session;
315                else
316                        $page = array();
317        }
318
319        // dezip si on l'a zipe
320        if (isset($page['gz'])) {
321                $page['texte'] = gzuncompress($page['texte']);
322                unset($page['gz']);
323        }
324
325        if (intval($GLOBALS['spip_version_branche'])<3){
326                // HEAD : cas sans jamais de calcul pour raisons de performance
327                // supprime en SPIP 3 par https://core.spip.net/projects/spip/repository/revisions/19959
328                if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
329                        $use_cache = 0;
330                        $page = array('contexte_implicite'=>$contexte_implicite);
331                        return;
332                }
333        }
334
335        // Si un calcul, recalcul [ou preview, mais c'est recalcul] est demande,
336        // on supprime le cache
337        if (((isset($GLOBALS['var_mode']) && $GLOBALS['var_mode']) OR (defined('_VAR_MODE') && _VAR_MODE)) &&
338                (isset($_COOKIE['spip_session'])
339                || isset($_COOKIE['spip_admin'])
340                || @file_exists(_ACCESS_FILE_NAME))
341        ) {
342                $page = array('contexte_implicite'=>$contexte_implicite); // ignorer le cache deja lu
343                include_spip('inc/invalideur');
344                if (function_exists('retire_caches')) retire_caches($chemin_cache); # API invalideur inutile
345                $memo->del($chemin_cache);
346                if ($chemin_cache_session)
347                        $memo->del($chemin_cache_session);
348        }
349
350        // $delais par defaut (pour toutes les pages sans #CACHE{})
351        if (!isset($GLOBALS['delais'])) {
352                if (!defined('_DUREE_CACHE_DEFAUT')) {
353                        define('_DUREE_CACHE_DEFAUT', 24*3600);
354                }
355                $GLOBALS['delais'] = _DUREE_CACHE_DEFAUT;
356        }
357
358        // determiner la validite de la page
359        if ($page) {
360                $use_cache = cache_valide($page, isset($page['lastmodified']) ? $page['lastmodified']:null);
361                // le contexte implicite n'est pas stocke dans le cache, mais il y a equivalence
362                // par le nom du cache. On le reinjecte donc ici pour utilisation eventuelle au calcul
363                $page['contexte_implicite'] = $contexte_implicite;
364                if (!$use_cache)
365                        return;
366        } else {
367                $page = array('contexte_implicite'=>$contexte_implicite);
368                $use_cache = cache_valide($page,0); // fichier cache absent : provoque le calcul
369        }
370
371        // Si pas valide mais pas de connexion a la base, le garder quand meme
372        if (!spip_connect()) {
373                if (isset($page['texte'])) {
374                        $use_cache = 0;
375                }
376                else {
377                        include_spip('inc/config');
378                        spip_log("Erreur base de donnees, impossible utiliser " . lire_config('memoization/methode') . " $chemin_cache");
379                        include_spip('inc/minipres');
380                        return minipres(_T('info_travaux_titre'),  _T('titre_probleme_technique'));
381                }
382        }
383
384        if ($use_cache < 0) $chemin_cache = '';
385        return;
386}
387
388// Faut-il decompresser ce cache ?
389// (passage par reference pour alleger)
390// https://code.spip.net/@gunzip_page
391function gunzip_page(&$page) {
392        if (!empty($page['gz'])) {
393                $page['texte'] = gzuncompress($page['texte']);
394                $page['gz'] = false; // ne pas gzuncompress deux fois une meme page
395        }
396}
Note: See TracBrowser for help on using the repository browser.