4a8209c00fc500bc42b588669be5c391a4aec471
[lhc/web/www.git] / www / ecrire / inc / flock.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2017 *
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
13 /**
14 * Gestion de recherche et d'écriture de répertoire ou fichiers
15 *
16 * @package SPIP\Core\Flock
17 **/
18
19 if (!defined('_ECRIRE_INC_VERSION')) {
20 return;
21 }
22
23
24 /**
25 * Autoriser la création de faux répertoires ?
26 *
27 * Ajouter `define('_CREER_DIR_PLAT', true);` dans mes_options pour restaurer
28 * le fonctionnement des faux répertoires en `.plat`
29 */
30 define('_CREER_DIR_PLAT', false);
31 if (!defined('_TEST_FILE_EXISTS')) {
32 /** Permettre d'éviter des tests file_exists sur certains hébergeurs */
33 define('_TEST_FILE_EXISTS', preg_match(',(online|free)[.]fr$,', isset($_ENV["HTTP_HOST"]) ? $_ENV["HTTP_HOST"] : ""));
34 }
35
36 #define('_SPIP_LOCK_MODE',0); // ne pas utiliser de lock (deconseille)
37 #define('_SPIP_LOCK_MODE',1); // utiliser le flock php
38 #define('_SPIP_LOCK_MODE',2); // utiliser le nfslock de spip
39
40 if (_SPIP_LOCK_MODE == 2) {
41 include_spip('inc/nfslock');
42 }
43
44 $GLOBALS['liste_verrous'] = array();
45
46 /**
47 * Ouvre un fichier et le vérrouille
48 *
49 * @link http://php.net/manual/fr/function.flock.php pour le type de verrou.
50 * @see _SPIP_LOCK_MODE
51 * @see spip_fclose_unlock()
52 * @uses spip_nfslock() si _SPIP_LOCK_MODE = 2.
53 *
54 * @param string $fichier
55 * Chemin du fichier
56 * @param string $mode
57 * Mode d'ouverture du fichier (r,w,...)
58 * @param string $verrou
59 * Type de verrou (avec _SPIP_LOCK_MODE = 1)
60 * @return Resource
61 * Ressource sur le fichier ouvert, sinon false.
62 **/
63 function spip_fopen_lock($fichier, $mode, $verrou) {
64 if (_SPIP_LOCK_MODE == 1) {
65 if ($fl = @fopen($fichier, $mode)) {
66 // verrou
67 @flock($fl, $verrou);
68 }
69
70 return $fl;
71 } elseif (_SPIP_LOCK_MODE == 2) {
72 if (($verrou = spip_nfslock($fichier)) && ($fl = @fopen($fichier, $mode))) {
73 $GLOBALS['liste_verrous'][$fl] = array($fichier, $verrou);
74
75 return $fl;
76 } else {
77 return false;
78 }
79 }
80
81 return @fopen($fichier, $mode);
82 }
83
84 /**
85 * Dévérrouille et ferme un fichier
86 *
87 * @see _SPIP_LOCK_MODE
88 * @see spip_fopen_lock()
89 *
90 * @param string $handle
91 * Chemin du fichier
92 * @return bool
93 * true si succès, false sinon.
94 **/
95 function spip_fclose_unlock($handle) {
96 if (_SPIP_LOCK_MODE == 1) {
97 @flock($handle, LOCK_UN);
98 } elseif (_SPIP_LOCK_MODE == 2) {
99 spip_nfsunlock(reset($GLOBALS['liste_verrous'][$handle]), end($GLOBALS['liste_verrous'][$handle]));
100 unset($GLOBALS['liste_verrous'][$handle]);
101 }
102
103 return @fclose($handle);
104 }
105
106
107 /**
108 * Retourne le contenu d'un fichier, même si celui ci est compréssé
109 * avec une extension en `.gz`
110 *
111 * @param string $fichier
112 * Chemin du fichier
113 * @return string
114 * Contenu du fichier
115 **/
116 function spip_file_get_contents($fichier) {
117 if (substr($fichier, -3) != '.gz') {
118 if (function_exists('file_get_contents')) {
119 // quand on est sous windows on ne sait pas si file_get_contents marche
120 // on essaye : si ca retourne du contenu alors c'est bon
121 // sinon on fait un file() pour avoir le coeur net
122 $contenu = @file_get_contents($fichier);
123 if (!$contenu and _OS_SERVEUR == 'windows') {
124 $contenu = @file($fichier);
125 }
126 } else {
127 $contenu = @file($fichier);
128 }
129 } else {
130 $contenu = @gzfile($fichier);
131 }
132
133 return is_array($contenu) ? join('', $contenu) : (string)$contenu;
134 }
135
136
137 /**
138 * Lit un fichier et place son contenu dans le paramètre transmis.
139 *
140 * Décompresse automatiquement les fichiers `.gz`
141 *
142 * @uses spip_fopen_lock()
143 * @uses spip_file_get_contents()
144 * @uses spip_fclose_unlock()
145 *
146 * @param string $fichier
147 * Chemin du fichier
148 * @param string $contenu
149 * Le contenu du fichier sera placé dans cette variable
150 * @param array $options
151 * Options tel que :
152 *
153 * - 'phpcheck' => 'oui' : vérifie qu'on a bien du php
154 * @return bool
155 * true si l'opération a réussie, false sinon.
156 **/
157 function lire_fichier($fichier, &$contenu, $options = array()) {
158 $contenu = '';
159 // inutile car si le fichier n'existe pas, le lock va renvoyer false juste apres
160 // economisons donc les acces disque, sauf chez free qui rale pour un rien
161 if (_TEST_FILE_EXISTS and !@file_exists($fichier)) {
162 return false;
163 }
164
165 #spip_timer('lire_fichier');
166
167 // pas de @ sur spip_fopen_lock qui est silencieux de toute facon
168 if ($fl = spip_fopen_lock($fichier, 'r', LOCK_SH)) {
169 // lire le fichier avant tout
170 $contenu = spip_file_get_contents($fichier);
171
172 // le fichier a-t-il ete supprime par le locker ?
173 // on ne verifie que si la tentative de lecture a echoue
174 // pour discriminer un contenu vide d'un fichier absent
175 // et eviter un acces disque
176 if (!$contenu and !@file_exists($fichier)) {
177 spip_fclose_unlock($fl);
178
179 return false;
180 }
181
182 // liberer le verrou
183 spip_fclose_unlock($fl);
184
185 // Verifications
186 $ok = true;
187 if (isset($options['phpcheck']) and $options['phpcheck'] == 'oui') {
188 $ok &= (preg_match(",[?]>\n?$,", $contenu));
189 }
190
191 #spip_log("$fread $fichier ".spip_timer('lire_fichier'));
192 if (!$ok) {
193 spip_log("echec lecture $fichier");
194 }
195
196 return $ok;
197 }
198
199 return false;
200 }
201
202
203 /**
204 * Écrit un fichier de manière un peu sûre
205 *
206 * Cette écriture s’exécute de façon sécurisée en posant un verrou sur
207 * le fichier avant sa modification. Les fichiers .gz sont compressés.
208 *
209 * @uses raler_fichier() Si le fichier n'a pu peut être écrit
210 * @see lire_fichier()
211 * @see supprimer_fichier()
212 *
213 * @param string $fichier
214 * Chemin du fichier
215 * @param string $contenu
216 * Contenu à écrire
217 * @param bool $ignorer_echec
218 * - true pour ne pas raler en cas d'erreur
219 * - false affichera un message si on est webmestre
220 * @param bool $truncate
221 * Écriture avec troncation ?
222 * @return bool
223 * - true si l’écriture s’est déroulée sans problème.
224 **/
225 function ecrire_fichier($fichier, $contenu, $ignorer_echec = false, $truncate = true) {
226
227 #spip_timer('ecrire_fichier');
228
229 // verrouiller le fichier destination
230 if ($fp = spip_fopen_lock($fichier, 'a', LOCK_EX)) {
231 // ecrire les donnees, compressees le cas echeant
232 // (on ouvre un nouveau pointeur sur le fichier, ce qui a l'avantage
233 // de le recreer si le locker qui nous precede l'avait supprime...)
234 if (substr($fichier, -3) == '.gz') {
235 $contenu = gzencode($contenu);
236 }
237 // si c'est une ecriture avec troncation , on fait plutot une ecriture complete a cote suivie unlink+rename
238 // pour etre sur d'avoir une operation atomique
239 // y compris en NFS : http://www.ietf.org/rfc/rfc1094.txt
240 // sauf sous wintruc ou ca ne marche pas
241 $ok = false;
242 if ($truncate and _OS_SERVEUR != 'windows') {
243 if (!function_exists('creer_uniqid')) {
244 include_spip('inc/acces');
245 }
246 $id = creer_uniqid();
247 // on ouvre un pointeur sur un fichier temporaire en ecriture +raz
248 if ($fp2 = spip_fopen_lock("$fichier.$id", 'w', LOCK_EX)) {
249 $s = @fputs($fp2, $contenu, $a = strlen($contenu));
250 $ok = ($s == $a);
251 spip_fclose_unlock($fp2);
252 spip_fclose_unlock($fp);
253 // unlink direct et pas spip_unlink car on avait deja le verrou
254 // a priori pas besoin car rename ecrase la cible
255 // @unlink($fichier);
256 // le rename aussitot, atomique quand on est pas sous windows
257 // au pire on arrive en second en cas de concourance, et le rename echoue
258 // --> on a la version de l'autre process qui doit etre identique
259 @rename("$fichier.$id", $fichier);
260 // precaution en cas d'echec du rename
261 if (!_TEST_FILE_EXISTS or @file_exists("$fichier.$id")) {
262 @unlink("$fichier.$id");
263 }
264 if ($ok) {
265 $ok = file_exists($fichier);
266 }
267 } else // echec mais penser a fermer ..
268 {
269 spip_fclose_unlock($fp);
270 }
271 }
272 // sinon ou si methode precedente a echoueee
273 // on se rabat sur la methode ancienne
274 if (!$ok) {
275 // ici on est en ajout ou sous windows, cas desespere
276 if ($truncate) {
277 @ftruncate($fp, 0);
278 }
279 $s = @fputs($fp, $contenu, $a = strlen($contenu));
280
281 $ok = ($s == $a);
282 spip_fclose_unlock($fp);
283 }
284
285 // liberer le verrou et fermer le fichier
286 @chmod($fichier, _SPIP_CHMOD & 0666);
287 if ($ok) {
288 if (strpos($fichier, ".php") !== false) {
289 spip_clear_opcode_cache(realpath($fichier));
290 }
291
292 return $ok;
293 }
294 }
295
296 if (!$ignorer_echec) {
297 include_spip('inc/autoriser');
298 if (autoriser('chargerftp')) {
299 raler_fichier($fichier);
300 }
301 spip_unlink($fichier);
302 }
303 spip_log("Ecriture fichier $fichier impossible", _LOG_INFO_IMPORTANTE);
304
305 return false;
306 }
307
308 /**
309 * Écrire un contenu dans un fichier encapsulé en PHP pour en empêcher l'accès en l'absence
310 * de fichier htaccess
311 *
312 * @uses ecrire_fichier()
313 *
314 * @param string $fichier
315 * Chemin du fichier
316 * @param string $contenu
317 * Contenu à écrire
318 * @param bool $ecrire_quand_meme
319 * - true pour ne pas raler en cas d'erreur
320 * - false affichera un message si on est webmestre
321 * @param bool $truncate
322 * Écriture avec troncation ?
323 */
324 function ecrire_fichier_securise($fichier, $contenu, $ecrire_quand_meme = false, $truncate = true) {
325 if (substr($fichier, -4) !== '.php') {
326 spip_log('Erreur de programmation: ' . $fichier . ' doit finir par .php');
327 }
328 $contenu = "<" . "?php die ('Acces interdit'); ?" . ">\n" . $contenu;
329
330 return ecrire_fichier($fichier, $contenu, $ecrire_quand_meme, $truncate);
331 }
332
333 /**
334 * Lire un fichier encapsulé en PHP
335 *
336 * @uses lire_fichier()
337 *
338 * @param string $fichier
339 * Chemin du fichier
340 * @param string $contenu
341 * Le contenu du fichier sera placé dans cette variable
342 * @param array $options
343 * Options tel que :
344 *
345 * - 'phpcheck' => 'oui' : vérifie qu'on a bien du php
346 * @return bool
347 * true si l'opération a réussie, false sinon.
348 */
349 function lire_fichier_securise($fichier, &$contenu, $options = array()) {
350 if ($res = lire_fichier($fichier, $contenu, $options)) {
351 $contenu = substr($contenu, strlen("<" . "?php die ('Acces interdit'); ?" . ">\n"));
352 }
353
354 return $res;
355 }
356
357 /**
358 * Affiche un message d’erreur bloquant, indiquant qu’il n’est pas possible de créer
359 * le fichier à cause des droits sur le répertoire parent au fichier.
360 *
361 * Arrête le script PHP par un exit;
362 *
363 * @uses minipres() Pour afficher le message
364 *
365 * @param string $fichier
366 * Chemin du fichier
367 **/
368 function raler_fichier($fichier) {
369 include_spip('inc/minipres');
370 $dir = dirname($fichier);
371 http_status(401);
372 echo minipres(_T('texte_inc_meta_2'), "<h4 style='color: red'>"
373 . _T('texte_inc_meta_1', array('fichier' => $fichier))
374 . " <a href='"
375 . generer_url_ecrire('install', "etape=chmod&test_dir=$dir")
376 . "'>"
377 . _T('texte_inc_meta_2')
378 . "</a> "
379 . _T('texte_inc_meta_3',
380 array('repertoire' => joli_repertoire($dir)))
381 . "</h4>\n");
382 exit;
383 }
384
385
386 /**
387 * Teste si un fichier est récent (moins de n secondes)
388 *
389 * @param string $fichier
390 * Chemin du fichier
391 * @param int $n
392 * Âge testé, en secondes
393 * @return bool
394 * - true si récent, false sinon
395 */
396 function jeune_fichier($fichier, $n) {
397 if (!file_exists($fichier)) {
398 return false;
399 }
400 if (!$c = @filemtime($fichier)) {
401 return false;
402 }
403
404 return (time() - $n <= $c);
405 }
406
407 /**
408 * Supprimer un fichier de manière sympa (flock)
409 *
410 * @param string $fichier
411 * Chemin du fichier
412 * @param bool $lock
413 * true pour utiliser un verrou
414 * @return bool|void
415 * - true si le fichier n'existe pas
416 * - false si on n'arrive pas poser le verrou
417 * - void sinon
418 */
419 function supprimer_fichier($fichier, $lock = true) {
420 if (!@file_exists($fichier)) {
421 return true;
422 }
423
424 if ($lock) {
425 // verrouiller le fichier destination
426 if (!$fp = spip_fopen_lock($fichier, 'a', LOCK_EX)) {
427 return false;
428 }
429
430 // liberer le verrou
431 spip_fclose_unlock($fp);
432 }
433
434 // supprimer
435 return @unlink($fichier);
436 }
437
438 /**
439 * Supprimer brutalement un fichier, s'il existe
440 *
441 * @param string $f
442 * Chemin du fichier
443 */
444 function spip_unlink($f) {
445 if (!is_dir($f)) {
446 supprimer_fichier($f, false);
447 } else {
448 @unlink("$f/.ok");
449 @rmdir($f);
450 }
451 }
452
453 /**
454 * Invalidates a PHP file from any active opcode caches.
455 *
456 * If the opcode cache does not support the invalidation of individual files,
457 * the entire cache will be flushed.
458 * kudo : http://cgit.drupalcode.org/drupal/commit/?id=be97f50
459 *
460 * @param string $filepath
461 * The absolute path of the PHP file to invalidate.
462 */
463 function spip_clear_opcode_cache($filepath) {
464 clearstatcache(true, $filepath);
465
466 // Zend OPcache
467 if (function_exists('opcache_invalidate')) {
468 opcache_invalidate($filepath, true);
469 }
470 // APC.
471 if (function_exists('apc_delete_file')) {
472 // apc_delete_file() throws a PHP warning in case the specified file was
473 // not compiled yet.
474 // @see http://php.net/apc-delete-file
475 @apc_delete_file($filepath);
476 }
477 }
478
479 /**
480 * Attendre l'invalidation de l'opcache
481 *
482 * Si opcache est actif et en mode `validate_timestamps`,
483 * le timestamp du fichier ne sera vérifié qu'après une durée
484 * en secondes fixée par `revalidate_freq`.
485 *
486 * Il faut donc attendre ce temps là pour être sûr qu'on va bien
487 * bénéficier de la recompilation du fichier par l'opcache.
488 *
489 * Ne fait rien en dehors de ce cas
490 *
491 * @note
492 * C'est une config foireuse déconseillée de opcode cache mais
493 * malheureusement utilisée par Octave.
494 * @link http://stackoverflow.com/questions/25649416/when-exactly-does-php-5-5-opcache-check-file-timestamp-based-on-revalidate-freq
495 * @link http://wiki.mikejung.biz/PHP_OPcache
496 *
497 */
498 function spip_attend_invalidation_opcode_cache() {
499 if (function_exists('opcache_get_configuration')
500 and @ini_get('opcache.enable')
501 and @ini_get('opcache.validate_timestamps')
502 and $duree = @ini_get('opcache.revalidate_freq')
503 ) {
504 sleep($duree + 1);
505 }
506 }
507
508
509 /**
510 * Suppression complete d'un repertoire.
511 *
512 * @link http://www.php.net/manual/en/function.rmdir.php#92050
513 *
514 * @param string $dir Chemin du repertoire
515 * @return bool Suppression reussie.
516 */
517 function supprimer_repertoire($dir) {
518 if (!file_exists($dir)) {
519 return true;
520 }
521 if (!is_dir($dir) || is_link($dir)) {
522 return @unlink($dir);
523 }
524
525 foreach (scandir($dir) as $item) {
526 if ($item == '.' || $item == '..') {
527 continue;
528 }
529 if (!supprimer_repertoire($dir . "/" . $item)) {
530 @chmod($dir . "/" . $item, 0777);
531 if (!supprimer_repertoire($dir . "/" . $item)) {
532 return false;
533 }
534 };
535 }
536
537 return @rmdir($dir);
538 }
539
540
541 /**
542 * Crée un sous répertoire
543 *
544 * Retourne `$base/${subdir}/` si le sous-repertoire peut être crée,
545 * `$base/${subdir}_` sinon.
546 *
547 * @example
548 * ```
549 * sous_repertoire(_DIR_CACHE, 'demo');
550 * sous_repertoire(_DIR_CACHE . '/demo');
551 * ```
552 *
553 * @param string $base
554 * - Chemin du répertoire parent (avec $subdir)
555 * - sinon chemin du répertoire à créer
556 * @param string $subdir
557 * - Nom du sous répertoire à créer,
558 * - non transmis, `$subdir` vaut alors ce qui suit le dernier `/` dans `$base`
559 * @param bool $nobase
560 * true pour ne pas avoir le chemin du parent `$base/` dans le retour
561 * @param bool $tantpis
562 * true pour ne pas raler en cas de non création du répertoire
563 * @return string
564 * Chemin du répertoire créé.
565 **/
566 function sous_repertoire($base, $subdir = '', $nobase = false, $tantpis = false) {
567 static $dirs = array();
568
569 $base = str_replace("//", "/", $base);
570
571 # suppr le dernier caractere si c'est un / ou un _
572 $base = rtrim($base, '/_');
573
574 if (!strlen($subdir)) {
575 $n = strrpos($base, "/");
576 if ($n === false) {
577 return $nobase ? '' : ($base . '/');
578 }
579 $subdir = substr($base, $n + 1);
580 $base = substr($base, 0, $n + 1);
581 } else {
582 $base .= '/';
583 $subdir = str_replace("/", "", $subdir);
584 }
585
586 $baseaff = $nobase ? '' : $base;
587 if (isset($dirs[$base . $subdir])) {
588 return $baseaff . $dirs[$base . $subdir];
589 }
590
591
592 if (_CREER_DIR_PLAT and @file_exists("$base${subdir}.plat")) {
593 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
594 }
595
596 $path = $base . $subdir; # $path = 'IMG/distant/pdf' ou 'IMG/distant_pdf'
597
598 if (file_exists("$path/.ok")) {
599 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
600 }
601
602 @mkdir($path, _SPIP_CHMOD);
603 @chmod($path, _SPIP_CHMOD);
604
605 if (is_dir($path) && is_writable($path)) {
606 @touch("$path/.ok");
607 spip_log("creation $base$subdir/");
608
609 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
610 }
611
612 // en cas d'echec c'est peut etre tout simplement que le disque est plein :
613 // l'inode du fichier dir_test existe, mais impossible d'y mettre du contenu
614 // => sauf besoin express (define dans mes_options), ne pas creer le .plat
615 if (_CREER_DIR_PLAT
616 and $f = @fopen("$base${subdir}.plat", "w")
617 ) {
618 fclose($f);
619 } else {
620 spip_log("echec creation $base${subdir}");
621 if ($tantpis) {
622 return '';
623 }
624 if (!_DIR_RESTREINT) {
625 $base = preg_replace(',^' . _DIR_RACINE . ',', '', $base);
626 }
627 $base .= $subdir;
628 raler_fichier($base . '/.plat');
629 }
630 spip_log("faux sous-repertoire $base${subdir}");
631
632 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
633 }
634
635
636 /**
637 * Parcourt récursivement le repertoire `$dir`, et renvoie les
638 * fichiers dont le chemin vérifie le pattern (preg) donné en argument.
639 *
640 * En cas d'echec retourne un `array()` vide
641 *
642 * @example
643 * ```
644 * $x = preg_files('ecrire/data/', '[.]lock$');
645 * // $x array()
646 * ```
647 *
648 * @note
649 * Attention, afin de conserver la compatibilite avec les repertoires '.plat'
650 * si `$dir = 'rep/sous_rep_'` au lieu de `rep/sous_rep/` on scanne `rep/` et on
651 * applique un pattern `^rep/sous_rep_`
652 *
653 * @param string $dir
654 * Répertoire à parcourir
655 * @param int|string $pattern
656 * Expression régulière pour trouver des fichiers, tel que `[.]lock$`
657 * @param int $maxfiles
658 * Nombre de fichiers maximums retournés
659 * @param array $recurs
660 * false pour ne pas descendre dans les sous répertoires
661 * @return array
662 * Chemins des fichiers trouvés.
663 **/
664 function preg_files($dir, $pattern = -1 /* AUTO */, $maxfiles = 10000, $recurs = array()) {
665 $nbfiles = 0;
666 if ($pattern == -1) {
667 $pattern = "^$dir";
668 }
669 $fichiers = array();
670 // revenir au repertoire racine si on a recu dossier/truc
671 // pour regarder dossier/truc/ ne pas oublier le / final
672 $dir = preg_replace(',/[^/]*$,', '', $dir);
673 if ($dir == '') {
674 $dir = '.';
675 }
676
677 if (@is_dir($dir) and is_readable($dir) and $d = opendir($dir)) {
678 while (($f = readdir($d)) !== false && ($nbfiles < $maxfiles)) {
679 if ($f[0] != '.' # ignorer . .. .svn etc
680 and $f != 'CVS'
681 and $f != 'remove.txt'
682 and is_readable($f = "$dir/$f")
683 ) {
684 if (is_file($f)) {
685 if (preg_match(";$pattern;iS", $f)) {
686 $fichiers[] = $f;
687 $nbfiles++;
688 }
689 } else {
690 if (is_dir($f) and is_array($recurs)) {
691 $rp = @realpath($f);
692 if (!is_string($rp) or !strlen($rp)) {
693 $rp = $f;
694 } # realpath n'est peut etre pas autorise
695 if (!isset($recurs[$rp])) {
696 $recurs[$rp] = true;
697 $beginning = $fichiers;
698 $end = preg_files("$f/", $pattern,
699 $maxfiles - $nbfiles, $recurs);
700 $fichiers = array_merge((array)$beginning, (array)$end);
701 $nbfiles = count($fichiers);
702 }
703 }
704 }
705 }
706 }
707 closedir($d);
708 }
709 sort($fichiers);
710
711 return $fichiers;
712 }