3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
6 * Copyright (c) 2001-2019 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
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 \***************************************************************************/
14 * Gestion de recherche et d'écriture de répertoire ou fichiers
16 * @package SPIP\Core\Flock
19 if (!defined('_ECRIRE_INC_VERSION')) {
25 * Autoriser la création de faux répertoires ?
27 * Ajouter `define('_CREER_DIR_PLAT', true);` dans mes_options pour restaurer
28 * le fonctionnement des faux répertoires en `.plat`
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"] : ""));
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
40 if (_SPIP_LOCK_MODE
== 2) {
41 include_spip('inc/nfslock');
44 $GLOBALS['liste_verrous'] = array();
47 * Ouvre un fichier et le vérrouille
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.
54 * @param string $fichier
57 * Mode d'ouverture du fichier (r,w,...)
58 * @param string $verrou
59 * Type de verrou (avec _SPIP_LOCK_MODE = 1)
61 * Ressource sur le fichier ouvert, sinon false.
63 function spip_fopen_lock($fichier, $mode, $verrou) {
64 if (_SPIP_LOCK_MODE
== 1) {
65 if ($fl = @fopen
($fichier, $mode)) {
71 } elseif (_SPIP_LOCK_MODE
== 2) {
72 if (($verrou = spip_nfslock($fichier)) && ($fl = @fopen
($fichier, $mode))) {
73 $GLOBALS['liste_verrous'][$fl] = array($fichier, $verrou);
81 return @fopen
($fichier, $mode);
85 * Dévérrouille et ferme un fichier
87 * @see _SPIP_LOCK_MODE
88 * @see spip_fopen_lock()
90 * @param string $handle
93 * true si succès, false sinon.
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]);
103 return @fclose
($handle);
108 * Retourne le contenu d'un fichier, même si celui ci est compréssé
109 * avec une extension en `.gz`
111 * @param string $fichier
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);
127 $contenu = @file
($fichier);
130 $contenu = @gzfile
($fichier);
133 return is_array($contenu) ?
join('', $contenu) : (string)$contenu;
138 * Lit un fichier et place son contenu dans le paramètre transmis.
140 * Décompresse automatiquement les fichiers `.gz`
142 * @uses spip_fopen_lock()
143 * @uses spip_file_get_contents()
144 * @uses spip_fclose_unlock()
146 * @param string $fichier
148 * @param string $contenu
149 * Le contenu du fichier sera placé dans cette variable
150 * @param array $options
153 * - 'phpcheck' => 'oui' : vérifie qu'on a bien du php
155 * true si l'opération a réussie, false sinon.
157 function lire_fichier($fichier, &$contenu, $options = array()) {
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)) {
165 #spip_timer('lire_fichier');
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);
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);
183 spip_fclose_unlock($fl);
187 if (isset($options['phpcheck']) and $options['phpcheck'] == 'oui') {
188 $ok &= (preg_match(",[?]>\n?$,", $contenu));
191 #spip_log("$fread $fichier ".spip_timer('lire_fichier'));
193 spip_log("echec lecture $fichier");
204 * Écrit un fichier de manière un peu sûre
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.
209 * @uses raler_fichier() Si le fichier n'a pu peut être écrit
210 * @see lire_fichier()
211 * @see supprimer_fichier()
213 * @param string $fichier
215 * @param string $contenu
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 ?
223 * - true si l’écriture s’est déroulée sans problème.
225 function ecrire_fichier($fichier, $contenu, $ignorer_echec = false, $truncate = true) {
227 #spip_timer('ecrire_fichier');
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);
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
242 if ($truncate and _OS_SERVEUR
!= 'windows') {
243 if (!function_exists('creer_uniqid')) {
244 include_spip('inc/acces');
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));
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");
265 $ok = file_exists($fichier);
267 } else // echec mais penser a fermer ..
269 spip_fclose_unlock($fp);
272 // sinon ou si methode precedente a echoueee
273 // on se rabat sur la methode ancienne
275 // ici on est en ajout ou sous windows, cas desespere
279 $s = @fputs
($fp, $contenu, $a = strlen($contenu));
282 spip_fclose_unlock($fp);
285 // liberer le verrou et fermer le fichier
286 @chmod
($fichier, _SPIP_CHMOD
& 0666);
288 if (strpos($fichier, ".php") !== false) {
289 spip_clear_opcode_cache(realpath($fichier));
296 if (!$ignorer_echec) {
297 include_spip('inc/autoriser');
298 if (autoriser('chargerftp')) {
299 raler_fichier($fichier);
301 spip_unlink($fichier);
303 spip_log("Ecriture fichier $fichier impossible", _LOG_INFO_IMPORTANTE
);
309 * Écrire un contenu dans un fichier encapsulé en PHP pour en empêcher l'accès en l'absence
310 * de fichier htaccess
312 * @uses ecrire_fichier()
314 * @param string $fichier
316 * @param string $contenu
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 ?
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');
328 $contenu = "<" . "?php die ('Acces interdit'); ?" . ">\n" . $contenu;
330 return ecrire_fichier($fichier, $contenu, $ecrire_quand_meme, $truncate);
334 * Lire un fichier encapsulé en PHP
336 * @uses lire_fichier()
338 * @param string $fichier
340 * @param string $contenu
341 * Le contenu du fichier sera placé dans cette variable
342 * @param array $options
345 * - 'phpcheck' => 'oui' : vérifie qu'on a bien du php
347 * true si l'opération a réussie, false sinon.
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"));
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.
361 * Arrête le script PHP par un exit;
363 * @uses minipres() Pour afficher le message
365 * @param string $fichier
368 function raler_fichier($fichier) {
369 include_spip('inc/minipres');
370 $dir = dirname($fichier);
372 echo minipres(_T('texte_inc_meta_2'), "<h4 style='color: red'>"
373 . _T('texte_inc_meta_1', array('fichier' => $fichier))
375 . generer_url_ecrire('install', "etape=chmod&test_dir=$dir")
377 . _T('texte_inc_meta_2')
379 . _T('texte_inc_meta_3',
380 array('repertoire' => joli_repertoire($dir)))
387 * Teste si un fichier est récent (moins de n secondes)
389 * @param string $fichier
392 * Âge testé, en secondes
394 * - true si récent, false sinon
396 function jeune_fichier($fichier, $n) {
397 if (!file_exists($fichier)) {
400 if (!$c = @filemtime
($fichier)) {
404 return (time() - $n <= $c);
408 * Supprimer un fichier de manière sympa (flock)
410 * @param string $fichier
413 * true pour utiliser un verrou
415 * - true si le fichier n'existe pas
416 * - false si on n'arrive pas poser le verrou
419 function supprimer_fichier($fichier, $lock = true) {
420 if (!@file_exists
($fichier)) {
425 // verrouiller le fichier destination
426 if (!$fp = spip_fopen_lock($fichier, 'a', LOCK_EX
)) {
431 spip_fclose_unlock($fp);
435 return @unlink
($fichier);
439 * Supprimer brutalement un fichier, s'il existe
444 function spip_unlink($f) {
446 supprimer_fichier($f, false);
454 * Invalidates a PHP file from any active opcode caches.
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
460 * @param string $filepath
461 * The absolute path of the PHP file to invalidate.
463 function spip_clear_opcode_cache($filepath) {
464 clearstatcache(true, $filepath);
467 if (function_exists('opcache_invalidate')) {
468 $invalidate = @opcache_invalidate
($filepath, true);
469 // si l'invalidation a echoue lever un flag
470 if (!$invalidate and !defined('_spip_attend_invalidation_opcode_cache')) {
471 define('_spip_attend_invalidation_opcode_cache',true);
473 } elseif (!defined('_spip_attend_invalidation_opcode_cache')) {
474 // n'agira que si opcache est effectivement actif (il semble qu'on a pas toujours la fonction opcache_invalidate)
475 define('_spip_attend_invalidation_opcode_cache',true);
478 if (function_exists('apc_delete_file')) {
479 // apc_delete_file() throws a PHP warning in case the specified file was
481 // @see http://php.net/apc-delete-file
482 @apc_delete_file
($filepath);
487 * Attendre l'invalidation de l'opcache
489 * Si opcache est actif et en mode `validate_timestamps`,
490 * le timestamp du fichier ne sera vérifié qu'après une durée
491 * en secondes fixée par `revalidate_freq`.
493 * Il faut donc attendre ce temps là pour être sûr qu'on va bien
494 * bénéficier de la recompilation du fichier par l'opcache.
496 * Ne fait rien en dehors de ce cas
499 * C'est une config foireuse déconseillée de opcode cache mais
500 * malheureusement utilisée par Octave.
501 * @link http://stackoverflow.com/questions/25649416/when-exactly-does-php-5-5-opcache-check-file-timestamp-based-on-revalidate-freq
502 * @link http://wiki.mikejung.biz/PHP_OPcache
505 function spip_attend_invalidation_opcode_cache($timestamp = null) {
506 if (function_exists('opcache_get_configuration')
507 and @ini_get
('opcache.enable')
508 and @ini_get
('opcache.validate_timestamps')
509 and ($duree = intval(@ini_get
('opcache.revalidate_freq')) or $duree = 2)
510 and defined('_spip_attend_invalidation_opcode_cache') // des invalidations ont echouees
514 $wait -= (time() - $timestamp);
519 spip_log('Probleme de configuration opcache.revalidate_freq '. $duree .'s : on attend '.$wait.'s', _LOG_INFO_IMPORTANTE
);
528 * Suppression complete d'un repertoire.
530 * @link http://www.php.net/manual/en/function.rmdir.php#92050
532 * @param string $dir Chemin du repertoire
533 * @return bool Suppression reussie.
535 function supprimer_repertoire($dir) {
536 if (!file_exists($dir)) {
539 if (!is_dir($dir) ||
is_link($dir)) {
540 return @unlink
($dir);
543 foreach (scandir($dir) as $item) {
544 if ($item == '.' ||
$item == '..') {
547 if (!supprimer_repertoire($dir . "/" . $item)) {
548 @chmod
($dir . "/" . $item, 0777);
549 if (!supprimer_repertoire($dir . "/" . $item)) {
560 * Crée un sous répertoire
562 * Retourne `$base/${subdir}/` si le sous-repertoire peut être crée,
563 * `$base/${subdir}_` sinon.
567 * sous_repertoire(_DIR_CACHE, 'demo');
568 * sous_repertoire(_DIR_CACHE . '/demo');
571 * @param string $base
572 * - Chemin du répertoire parent (avec $subdir)
573 * - sinon chemin du répertoire à créer
574 * @param string $subdir
575 * - Nom du sous répertoire à créer,
576 * - non transmis, `$subdir` vaut alors ce qui suit le dernier `/` dans `$base`
577 * @param bool $nobase
578 * true pour ne pas avoir le chemin du parent `$base/` dans le retour
579 * @param bool $tantpis
580 * true pour ne pas raler en cas de non création du répertoire
582 * Chemin du répertoire créé.
584 function sous_repertoire($base, $subdir = '', $nobase = false, $tantpis = false) {
585 static $dirs = array();
587 $base = str_replace("//", "/", $base);
589 # suppr le dernier caractere si c'est un / ou un _
590 $base = rtrim($base, '/');
591 if (_CREER_DIR_PLAT
) {
592 $base = rtrim($base, '_');
595 if (!strlen($subdir)) {
596 $n = strrpos($base, "/");
598 return $nobase ?
'' : ($base . '/');
600 $subdir = substr($base, $n +
1);
601 $base = substr($base, 0, $n +
1);
604 $subdir = str_replace("/", "", $subdir);
605 if (_CREER_DIR_PLAT
) {
606 $subdir = rtrim($subdir, '_');
610 $baseaff = $nobase ?
'' : $base;
611 if (isset($dirs[$base . $subdir])) {
612 return $baseaff . $dirs[$base . $subdir];
616 if (_CREER_DIR_PLAT
and @file_exists
("$base${subdir}.plat")) {
617 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
620 $path = $base . $subdir; # $path = 'IMG/distant/pdf' ou 'IMG/distant_pdf'
622 if (file_exists("$path/.ok")) {
623 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
626 @mkdir
($path, _SPIP_CHMOD
);
627 @chmod
($path, _SPIP_CHMOD
);
629 if (is_dir($path) && is_writable($path)) {
631 spip_log("creation $base$subdir/");
633 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
636 // en cas d'echec c'est peut etre tout simplement que le disque est plein :
637 // l'inode du fichier dir_test existe, mais impossible d'y mettre du contenu
638 // => sauf besoin express (define dans mes_options), ne pas creer le .plat
640 and $f = @fopen
("$base${subdir}.plat", "w")
644 spip_log("echec creation $base${subdir}");
648 if (!_DIR_RESTREINT
) {
649 $base = preg_replace(',^' . _DIR_RACINE
. ',', '', $base);
652 raler_fichier($base . '/.plat');
654 spip_log("faux sous-repertoire $base${subdir}");
656 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
661 * Parcourt récursivement le repertoire `$dir`, et renvoie les
662 * fichiers dont le chemin vérifie le pattern (preg) donné en argument.
664 * En cas d'echec retourne un `array()` vide
668 * $x = preg_files('ecrire/data/', '[.]lock$');
673 * Attention, afin de conserver la compatibilite avec les repertoires '.plat'
674 * si `$dir = 'rep/sous_rep_'` au lieu de `rep/sous_rep/` on scanne `rep/` et on
675 * applique un pattern `^rep/sous_rep_`
678 * Répertoire à parcourir
679 * @param int|string $pattern
680 * Expression régulière pour trouver des fichiers, tel que `[.]lock$`
681 * @param int $maxfiles
682 * Nombre de fichiers maximums retournés
683 * @param array $recurs
684 * false pour ne pas descendre dans les sous répertoires
686 * Chemins des fichiers trouvés.
688 function preg_files($dir, $pattern = -1 /* AUTO */, $maxfiles = 10000, $recurs = array()) {
690 if ($pattern == -1) {
694 // revenir au repertoire racine si on a recu dossier/truc
695 // pour regarder dossier/truc/ ne pas oublier le / final
696 $dir = preg_replace(',/[^/]*$,', '', $dir);
701 if (@is_dir
($dir) and is_readable($dir) and $d = opendir($dir)) {
702 while (($f = readdir($d)) !== false && ($nbfiles < $maxfiles)) {
703 if ($f[0] != '.' # ignorer . .. .svn etc
705 and $f != 'remove.txt'
706 and is_readable($f = "$dir/$f")
709 if (preg_match(";$pattern;iS", $f)) {
714 if (is_dir($f) and is_array($recurs)) {
716 if (!is_string($rp) or !strlen($rp)) {
718 } # realpath n'est peut etre pas autorise
719 if (!isset($recurs[$rp])) {
721 $beginning = $fichiers;
722 $end = preg_files("$f/", $pattern,
723 $maxfiles - $nbfiles, $recurs);
724 $fichiers = array_merge((array)$beginning, (array)$end);
725 $nbfiles = count($fichiers);