3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
6 * Copyright (c) 2001-2017 *
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 opcache_invalidate($filepath, true);
471 if (function_exists('apc_delete_file')) {
472 // apc_delete_file() throws a PHP warning in case the specified file was
474 // @see http://php.net/apc-delete-file
475 @apc_delete_file
($filepath);
480 * Attendre l'invalidation de l'opcache
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`.
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.
489 * Ne fait rien en dehors de ce cas
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
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')
504 spip_log('Probleme de configuration opcache.revalidate_freq '. $duree .'s', _LOG_INFO_IMPORTANTE
);
511 * Suppression complete d'un repertoire.
513 * @link http://www.php.net/manual/en/function.rmdir.php#92050
515 * @param string $dir Chemin du repertoire
516 * @return bool Suppression reussie.
518 function supprimer_repertoire($dir) {
519 if (!file_exists($dir)) {
522 if (!is_dir($dir) ||
is_link($dir)) {
523 return @unlink
($dir);
526 foreach (scandir($dir) as $item) {
527 if ($item == '.' ||
$item == '..') {
530 if (!supprimer_repertoire($dir . "/" . $item)) {
531 @chmod
($dir . "/" . $item, 0777);
532 if (!supprimer_repertoire($dir . "/" . $item)) {
543 * Crée un sous répertoire
545 * Retourne `$base/${subdir}/` si le sous-repertoire peut être crée,
546 * `$base/${subdir}_` sinon.
550 * sous_repertoire(_DIR_CACHE, 'demo');
551 * sous_repertoire(_DIR_CACHE . '/demo');
554 * @param string $base
555 * - Chemin du répertoire parent (avec $subdir)
556 * - sinon chemin du répertoire à créer
557 * @param string $subdir
558 * - Nom du sous répertoire à créer,
559 * - non transmis, `$subdir` vaut alors ce qui suit le dernier `/` dans `$base`
560 * @param bool $nobase
561 * true pour ne pas avoir le chemin du parent `$base/` dans le retour
562 * @param bool $tantpis
563 * true pour ne pas raler en cas de non création du répertoire
565 * Chemin du répertoire créé.
567 function sous_repertoire($base, $subdir = '', $nobase = false, $tantpis = false) {
568 static $dirs = array();
570 $base = str_replace("//", "/", $base);
572 # suppr le dernier caractere si c'est un / ou un _
573 $base = rtrim($base, '/_');
575 if (!strlen($subdir)) {
576 $n = strrpos($base, "/");
578 return $nobase ?
'' : ($base . '/');
580 $subdir = substr($base, $n +
1);
581 $base = substr($base, 0, $n +
1);
584 $subdir = str_replace("/", "", $subdir);
587 $baseaff = $nobase ?
'' : $base;
588 if (isset($dirs[$base . $subdir])) {
589 return $baseaff . $dirs[$base . $subdir];
593 if (_CREER_DIR_PLAT
and @file_exists
("$base${subdir}.plat")) {
594 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
597 $path = $base . $subdir; # $path = 'IMG/distant/pdf' ou 'IMG/distant_pdf'
599 if (file_exists("$path/.ok")) {
600 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
603 @mkdir
($path, _SPIP_CHMOD
);
604 @chmod
($path, _SPIP_CHMOD
);
606 if (is_dir($path) && is_writable($path)) {
608 spip_log("creation $base$subdir/");
610 return $baseaff . ($dirs[$base . $subdir] = "$subdir/");
613 // en cas d'echec c'est peut etre tout simplement que le disque est plein :
614 // l'inode du fichier dir_test existe, mais impossible d'y mettre du contenu
615 // => sauf besoin express (define dans mes_options), ne pas creer le .plat
617 and $f = @fopen
("$base${subdir}.plat", "w")
621 spip_log("echec creation $base${subdir}");
625 if (!_DIR_RESTREINT
) {
626 $base = preg_replace(',^' . _DIR_RACINE
. ',', '', $base);
629 raler_fichier($base . '/.plat');
631 spip_log("faux sous-repertoire $base${subdir}");
633 return $baseaff . ($dirs[$base . $subdir] = "${subdir}_");
638 * Parcourt récursivement le repertoire `$dir`, et renvoie les
639 * fichiers dont le chemin vérifie le pattern (preg) donné en argument.
641 * En cas d'echec retourne un `array()` vide
645 * $x = preg_files('ecrire/data/', '[.]lock$');
650 * Attention, afin de conserver la compatibilite avec les repertoires '.plat'
651 * si `$dir = 'rep/sous_rep_'` au lieu de `rep/sous_rep/` on scanne `rep/` et on
652 * applique un pattern `^rep/sous_rep_`
655 * Répertoire à parcourir
656 * @param int|string $pattern
657 * Expression régulière pour trouver des fichiers, tel que `[.]lock$`
658 * @param int $maxfiles
659 * Nombre de fichiers maximums retournés
660 * @param array $recurs
661 * false pour ne pas descendre dans les sous répertoires
663 * Chemins des fichiers trouvés.
665 function preg_files($dir, $pattern = -1 /* AUTO */, $maxfiles = 10000, $recurs = array()) {
667 if ($pattern == -1) {
671 // revenir au repertoire racine si on a recu dossier/truc
672 // pour regarder dossier/truc/ ne pas oublier le / final
673 $dir = preg_replace(',/[^/]*$,', '', $dir);
678 if (@is_dir
($dir) and is_readable($dir) and $d = opendir($dir)) {
679 while (($f = readdir($d)) !== false && ($nbfiles < $maxfiles)) {
680 if ($f[0] != '.' # ignorer . .. .svn etc
682 and $f != 'remove.txt'
683 and is_readable($f = "$dir/$f")
686 if (preg_match(";$pattern;iS", $f)) {
691 if (is_dir($f) and is_array($recurs)) {
693 if (!is_string($rp) or !strlen($rp)) {
695 } # realpath n'est peut etre pas autorise
696 if (!isset($recurs[$rp])) {
698 $beginning = $fichiers;
699 $end = preg_files("$f/", $pattern,
700 $maxfiles - $nbfiles, $recurs);
701 $fichiers = array_merge((array)$beginning, (array)$end);
702 $nbfiles = count($fichiers);