[SPIP][PLUGINS] v3.0-->v3.2
[lhc/web/www.git] / www / plugins-dist / compresseur / inc / compresseur.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2016 *
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 * Fonctions d'aide pour le compresseur
15 *
16 * @package SPIP\Compresseur\Fonctions
17 */
18 if (!defined("_ECRIRE_INC_VERSION")) {
19 return;
20 }
21
22 /**
23 * Ecrire la balise javascript pour insérer le fichier compressé
24 *
25 * C'est cette fonction qui décide où il est le plus pertinent
26 * d'insérer le fichier, et dans quelle forme d'ecriture
27 *
28 * @param string $flux
29 * Contenu du head nettoyé des fichiers qui ont été compressé
30 * @param int $pos
31 * Position initiale du premier fichier inclu dans le fichier compressé
32 * @param string $src
33 * Nom du fichier compressé
34 * @param string $comments
35 * Commentaires à insérer devant
36 * @return string
37 * Code HTML de la balise <script>
38 */
39 function compresseur_ecrire_balise_js_dist(&$flux, $pos, $src, $comments = "") {
40 $src = timestamp($src);
41 // option chargement JS async par jQl
42 if (defined('_JS_ASYNC_LOAD') and !test_espace_prive()) {
43 lire_fichier(find_in_path("lib/jQl/jQl.min.js"), $jQl);
44 if ($jQl) {
45 $comments .= "<script type='text/javascript'>\n$jQl\njQl.loadjQ('$src')\n</script>";
46 } else {
47 $comments .= "<script type='text/javascript' src='$src'></script>";
48 }
49 } else {
50 $comments .= "<script type='text/javascript' src='$src'></script>";
51 }
52
53 $flux = substr_replace($flux, $comments, $pos, 0);
54
55 return $flux;
56 }
57
58 /**
59 * Ecrire la balise CSS pour insérer le fichier compressé
60 *
61 * C'est cette fonction qui décide ou il est le plus pertinent
62 * d'insérer le fichier, et dans quelle forme d'écriture
63 *
64 * @param string $flux
65 * Contenu du head nettoyé des fichiers qui ont ete compressé
66 * @param int $pos
67 * Position initiale du premier fichier inclu dans le fichier compressé
68 * @param string $src
69 * Nom du fichier compressé
70 * @param string $comments
71 * Commentaires à insérer devant
72 * @param string $media
73 * Type de media si précisé (print|screen...)
74 * @return string
75 * Code HTML de la balise <link>
76 */
77 function compresseur_ecrire_balise_css_dist(&$flux, $pos, $src, $comments = "", $media = "") {
78 $src = timestamp($src);
79 $comments .= "<link rel='stylesheet'" . ($media ? " media='$media'" : "") . " href='$src' type='text/css' />";
80 // Envoyer aussi un entete http pour demarer le chargement de la CSS plus tot
81 // Link: <http://href.here/to/resource.html>;rel="stylesheet prefetch"
82 $comments .= "<" . "?php header('Link: <' . url_de_base() . (_DIR_RACINE ? _DIR_RESTREINT_ABS : '') . '$src>;rel=\"stylesheet prefetch\"'); ?>";
83 $flux = substr_replace($flux, $comments, $pos, 0);
84
85 return $flux;
86 }
87
88 /**
89 * Extraire les balises CSS à compacter
90 *
91 * @param string $flux
92 * Contenu HTML dont on extrait les balises CSS
93 * @param string $url_base
94 * @return array
95 * Couples (balise => src)
96 */
97 function compresseur_extraire_balises_css_dist($flux, $url_base) {
98 $balises = extraire_balises($flux, 'link');
99 $files = array();
100 foreach ($balises as $s) {
101 if (extraire_attribut($s, 'rel') === 'stylesheet'
102 and (!($type = extraire_attribut($s, 'type'))
103 or $type == 'text/css')
104 and is_null(extraire_attribut($s, 'name')) # css nommee : pas touche
105 and is_null(extraire_attribut($s, 'id')) # idem
106 and !strlen(strip_tags($s))
107 and $src = preg_replace(",^$url_base,", _DIR_RACINE, extraire_attribut($s, 'href'))
108 ) {
109 $files[$s] = $src;
110 }
111 }
112
113 return $files;
114 }
115
116 /**
117 * Extraire les balises JS à compacter
118 *
119 * @param string $flux
120 * Contenu HTML dont on extrait les balises CSS
121 * @param string $url_base
122 * @return array
123 * Couples (balise => src)
124 */
125 function compresseur_extraire_balises_js_dist($flux, $url_base) {
126 $balises = extraire_balises($flux, 'script');
127 $files = array();
128 foreach ($balises as $s) {
129 if (extraire_attribut($s, 'type') === 'text/javascript'
130 and is_null(extraire_attribut($s, 'id')) # script avec un id : pas touche
131 and $src = extraire_attribut($s, 'src')
132 and !strlen(strip_tags($s))
133 ) {
134 $files[$s] = $src;
135 }
136 }
137
138 return $files;
139 }
140
141 /**
142 * Compacter (concaténer+minifier) les fichiers format CSS ou JS
143 * du head.
144 *
145 * Repérer fichiers statiques vs. url squelettes
146 * Compacte le tout dans un fichier statique posé dans local/
147 *
148 * @param string $flux
149 * Contenu du <head> de la page html
150 * @param string $format
151 * css ou js
152 * @return string
153 * Contenu compressé du <head> de la page html
154 */
155 function compacte_head_files($flux, $format) {
156 $url_base = url_de_base();
157 $url_page = substr(generer_url_public('A'), 0, -1);
158 $dir = preg_quote($url_page, ',') . '|' . preg_quote(preg_replace(",^$url_base,", _DIR_RACINE, $url_page), ',');
159
160 if (!$extraire_balises = charger_fonction("compresseur_extraire_balises_$format", '', true)) {
161 return $flux;
162 }
163
164 $files = array();
165 $flux_nocomment = preg_replace(",<!--.*-->,Uims", "", $flux);
166 foreach ($extraire_balises($flux_nocomment, $url_base) as $s => $src) {
167 if (
168 preg_match(',^(' . $dir . ')(.*)$,', $src, $r)
169 or (
170 // ou si c'est un fichier
171 $src = preg_replace(',^' . preg_quote(url_de_base(), ',') . ',', '', $src)
172 // enlever un timestamp eventuel derriere un nom de fichier statique
173 and $src2 = preg_replace(",[.]{$format}[?].+$,", ".$format", $src)
174 // verifier qu'il n'y a pas de ../ ni / au debut (securite)
175 and !preg_match(',(^/|\.\.),', substr($src, strlen(_DIR_RACINE)))
176 // et si il est lisible
177 and @is_readable($src2)
178 )
179 ) {
180 if ($r) {
181 $files[$s] = explode('&', str_replace('&amp;', '&', $r[2]), 2);
182 } else {
183 $files[$s] = $src;
184 }
185 }
186 }
187
188 $callbacks = array('each_min' => 'callback_minifier_' . $format . '_file');
189
190 if ($format == "css") {
191 $callbacks['each_pre'] = 'compresseur_callback_prepare_css';
192 $callbacks['all_min'] = 'css_regroup_atimport';
193 // ce n'est pas une callback, mais en injectant l'url de base ici
194 // on differencie les caches quand l'url de base change
195 // puisque la css compresse inclue l'url courante du site (en url absolue)
196 // on exclue le protocole car la compression se fait en url relative au protocole
197 $callbacks[] = protocole_implicite($url_base);
198 // et l'URL des ressources statiques si configuree
199 if (isset($GLOBALS['meta']['url_statique_ressources']) and $GLOBALS['meta']['url_statique_ressources']){
200 $callbacks[] = protocole_implicite($GLOBALS['meta']['url_statique_ressources']);
201 }
202 }
203
204 include_spip('inc/compresseur_concatener');
205 include_spip('inc/compresseur_minifier');
206 if (list($src, $comms) = concatener_fichiers($files, $format, $callbacks)
207 and $src
208 ) {
209 $compacte_ecrire_balise = charger_fonction("compresseur_ecrire_balise_$format", '');
210 $files = array_keys($files);
211 // retrouver la position du premier fichier compacte
212 $pos = strpos($flux, reset($files));
213 // supprimer tous les fichiers compactes du flux
214 $flux = str_replace($files, "", $flux);
215 // inserer la balise (deleguer a la fonction, en lui donnant le necessaire)
216 $flux = $compacte_ecrire_balise($flux, $pos, $src, $comms);
217 }
218
219 return $flux;
220 }
221
222
223 /**
224 * Lister les fonctions de préparation des feuilles css
225 * avant minification
226 *
227 * @return array
228 * Liste des fonctions à appliquer sur les feuilles CSS
229 */
230 function compresseur_liste_fonctions_prepare_css() {
231 static $fonctions = null;
232
233 if (is_null($fonctions)) {
234 $fonctions = array('css_resolve_atimport', 'urls_absolues_css', 'css_url_statique_ressources');
235 // les fonctions de preparation aux CSS peuvent etre personalisees
236 // via la globale $compresseur_filtres_css sous forme de tableau de fonctions ordonnees
237 if (isset($GLOBALS['compresseur_filtres_css']) and is_array($GLOBALS['compresseur_filtres_css'])) {
238 $fonctions = $GLOBALS['compresseur_filtres_css'] + $fonctions;
239 }
240 }
241
242 return $fonctions;
243 }
244
245
246 /**
247 * Préparer un fichier CSS avant sa minification
248 *
249 * @param string $css
250 * @param bool|string $is_inline
251 * @param string $fonctions
252 * @return bool|int|null|string
253 */
254 function &compresseur_callback_prepare_css(&$css, $is_inline = false, $fonctions = null) {
255 if ($is_inline) {
256 return compresseur_callback_prepare_css_inline($css, $is_inline);
257 }
258 if (!preg_match(',\.css$,i', $css, $r)) {
259 return $css;
260 }
261
262 $url_absolue_css = url_absolue($css);
263 // retirer le protocole de $url_absolue_css
264 $url_absolue_css_implicite = protocole_implicite($url_absolue_css);
265
266 if (!$fonctions) {
267 $fonctions = compresseur_liste_fonctions_prepare_css();
268 } elseif (is_string($fonctions)) {
269 $fonctions = array($fonctions);
270 }
271
272 $sign = implode(",", $fonctions);
273 $sign = substr(md5("$url_absolue_css_implicite-$sign"), 0, 8);
274
275 $file = basename($css, '.css');
276 $file = sous_repertoire(_DIR_VAR, 'cache-css')
277 . preg_replace(",(.*?)(_rtl|_ltr)?$,", "\\1-f-" . $sign . "\\2", $file)
278 . '.css';
279
280 if ((@filemtime($file) > @filemtime($css))
281 and (!defined('_VAR_MODE') or _VAR_MODE != 'recalcul')
282 ) {
283 return $file;
284 }
285
286 if ($url_absolue_css == $css) {
287 if (strncmp($GLOBALS['meta']['adresse_site'] . "/", $css, $l = strlen($GLOBALS['meta']['adresse_site'] . "/")) != 0
288 or !lire_fichier(_DIR_RACINE . substr($css, $l), $contenu)
289 ) {
290 include_spip('inc/distant');
291 if (!$contenu = recuperer_page($css)) {
292 return $css;
293 }
294 }
295 } elseif (!lire_fichier($css, $contenu)) {
296 return $css;
297 }
298
299 $contenu = compresseur_callback_prepare_css_inline($contenu, $url_absolue_css_implicite, $css, $fonctions);
300
301 // ecrire la css
302 if (!ecrire_fichier($file, $contenu)) {
303 return $css;
304 }
305
306 return $file;
307 }
308
309 /**
310 * Préparer du contenu CSS inline avant minification
311 *
312 * @param string $contenu
313 * contenu de la CSS
314 * @param string $url_base
315 * url de la CSS ou de la page si c'est un style inline
316 * @param string $filename
317 * nom du fichier de la CSS (ou vide si c'est un style inline)
318 * @param array $fonctions
319 * liste des fonctions appliquees a la CSS
320 * @return string
321 */
322 function &compresseur_callback_prepare_css_inline(&$contenu, $url_base, $filename = '', $fonctions = null) {
323 if (!$fonctions) {
324 $fonctions = compresseur_liste_fonctions_prepare_css();
325 } elseif (is_string($fonctions)) {
326 $fonctions = array($fonctions);
327 }
328
329 // retirer le protocole de $url_base
330 $url_base = protocole_implicite(url_absolue($url_base));
331
332 foreach ($fonctions as $f) {
333 if (!function_exists($f)) {
334 $f = chercher_filtre($f);
335 }
336 if ($f and function_exists($f)) {
337 $contenu = $f($contenu, $url_base, $filename);
338 }
339 }
340
341 return $contenu;
342 }
343
344 /**
345 * Resoudre et inliner les @import
346 * ceux-ci ne peuvent etre presents qu'en debut de CSS et on ne veut pas changer l'ordre des directives
347 *
348 * @param string $contenu
349 * @param string $url_base
350 * @param string $filename
351 * @return string
352 */
353 function css_resolve_atimport($contenu, $url_base, $filename) {
354 // vite si rien a faire
355 if (strpos($contenu, "@import") === false) {
356 return $contenu;
357 }
358
359 $imports_non_resolvables = array();
360 preg_match_all(",@import ([^;]*);,UmsS", $contenu, $matches, PREG_SET_ORDER);
361
362 if ($matches and count($matches)) {
363 foreach ($matches as $m) {
364 $url = $media = $erreur = "";
365 if (preg_match(",^\s*url\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\),Ums", $m[1], $r)) {
366 $url = $r[1];
367 $media = trim(substr($m[1], strlen($r[0])));
368 } elseif (preg_match(",^\s*['\"]([^'\"]+)['\"],Ums", $m[1], $r)) {
369 $url = $r[1];
370 $media = trim(substr($m[1], strlen($r[0])));
371 }
372 if (!$url) {
373 $erreur = "Compresseur : <tt>" . $m[0] . ";</tt> non resolu dans <tt>$url_base</tt>";
374 } else {
375 $url = suivre_lien($url_base, $url);
376 // url relative ?
377 $root = protocole_implicite($GLOBALS['meta']['adresse_site'] . "/");
378 if (strncmp($url, $root, strlen($root)) == 0) {
379 $url = _DIR_RACINE . substr($url, strlen($root));
380 } else {
381 // si l'url a un protocole http(s):// on ne considère qu'on ne peut pas
382 // résoudre le stockage. Par exemple
383 // @import url(https://fonts.googleapis.com/css?family=Ubuntu);
384 // retournant un contenu différent en fonction navigateur
385 // tous les @import restant seront remontes en tete de CSS en fin de concatenation
386 if (preg_match(',^https?://,', $url)) {
387 $url = "";
388 } else {
389 // protocole implicite //
390 $url = "http:$url";
391 }
392 }
393
394 if ($url) {
395 // on renvoit dans la boucle pour que le fichier inclus
396 // soit aussi processe (@import, url absolue etc...)
397 $css = compresseur_callback_prepare_css($url);
398 if ($css == $url
399 or !lire_fichier($css, $contenu_imported)
400 ) {
401 $erreur = "Compresseur : url $url de <tt>" . $m[0] . ";</tt> non resolu dans <tt>$url_base</tt>";
402 } else {
403 if ($media) {
404 $contenu_imported = "@media $media{\n$contenu_imported\n}\n";
405 }
406 $contenu = str_replace($m[0], $contenu_imported, $contenu);
407 }
408 }
409 }
410
411 if ($erreur) {
412 $contenu = str_replace($m[0], "/* erreur @ import " . $m[1] . "*/", $contenu);
413 erreur_squelette($erreur);
414 }
415 }
416 }
417
418 return $contenu;
419 }
420
421 /**
422 * Regrouper les @import restants dans la CSS concatenee en debut de celle-ci
423 *
424 * @param string $nom_tmp
425 * @param string $nom
426 * @return bool|string
427 */
428 function css_regroup_atimport($nom_tmp, $nom) {
429 lire_fichier($nom_tmp, $contenu);
430 if (!$contenu or strpos($contenu, "@import") === false) {
431 return false;
432 } // rien a faire
433
434 preg_match_all(",@import ([^;]*);,UmsS", $contenu, $matches, PREG_SET_ORDER);
435 $imports = array_map("reset", $matches);
436 $contenu = str_replace($imports, "", $contenu);
437 $contenu = implode("\n", $imports) . "\n" . $contenu;
438 ecrire_fichier($nom, $contenu, true);
439 // ecrire une version .gz pour content-negociation par apache, cf. [11539]
440 ecrire_fichier("$nom.gz", $contenu, true);
441
442 return $nom;
443 }
444
445 /**
446 * Remplacer l'URL du site par une url de ressource genre static.example.org
447 * qui evite les echanges de cookie pour les ressources images
448 * (peut aussi etre l'URL d'un CDN ou autre provider de ressources statiques)
449 *
450 * @param string $contenu
451 * @param string $url_base
452 * @param string $filename
453 * @return mixed
454 */
455 function css_url_statique_ressources($contenu, $url_base, $filename){
456
457 if (isset($GLOBALS['meta']['url_statique_ressources'])
458 and $url_statique = $GLOBALS['meta']['url_statique_ressources']) {
459 $url_statique = rtrim(protocole_implicite($url_statique),"/")."/";
460 $url_site = rtrim(protocole_implicite($GLOBALS['meta']['adresse_site']),"/")."/";
461 $contenu = str_replace($url_site, $url_statique, $contenu);
462 }
463 return $contenu;
464 }