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 * Ce fichier gère l'obtention de données distantes
16 * @package SPIP\Core\Distant
18 if (!defined('_ECRIRE_INC_VERSION')) {
22 if (!defined('_INC_DISTANT_VERSION_HTTP')) {
23 define('_INC_DISTANT_VERSION_HTTP', 'HTTP/1.0');
25 if (!defined('_INC_DISTANT_CONTENT_ENCODING')) {
26 define('_INC_DISTANT_CONTENT_ENCODING', 'gzip');
28 if (!defined('_INC_DISTANT_USER_AGENT')) {
29 define('_INC_DISTANT_USER_AGENT', 'SPIP-' . $GLOBALS['spip_version_affichee'] . ' (' . $GLOBALS['home_server'] . ')');
31 if (!defined('_INC_DISTANT_MAX_SIZE')) {
32 define('_INC_DISTANT_MAX_SIZE', 2097152);
34 if (!defined('_INC_DISTANT_CONNECT_TIMEOUT')) {
35 define('_INC_DISTANT_CONNECT_TIMEOUT', 10);
38 define('_REGEXP_COPIE_LOCALE', ',' .
42 (isset($GLOBALS['meta']['adresse_site']) ?
$GLOBALS['meta']['adresse_site'] : '')
44 . '/?spip.php[?]action=acceder_document.*file=(.*)$,');
46 //@define('_COPIE_LOCALE_MAX_SIZE',2097152); // poids (inc/utils l'a fait)
49 * Crée au besoin la copie locale d'un fichier distant
51 * Prend en argument un chemin relatif au rep racine, ou une URL
52 * Renvoie un chemin relatif au rep racine, ou false
54 * @link http://www.spip.net/4155
55 * @pipeline_appel post_edition
57 * @param string $source
59 * - 'test' - ne faire que tester
60 * - 'auto' - charger au besoin
61 * - 'modif' - Si deja present, ne charger que si If-Modified-Since
62 * - 'force' - charger toujours (mettre a jour)
63 * @param string $local
64 * permet de specifier le nom du fichier local (stockage d'un cache par exemple, et non document IMG)
65 * @param int $taille_max
66 * taille maxi de la copie local, par defaut _COPIE_LOCALE_MAX_SIZE
69 function copie_locale($source, $mode = 'auto', $local = null, $taille_max = null) {
71 // si c'est la protection de soi-meme, retourner le path
72 if ($mode !== 'force' and preg_match(_REGEXP_COPIE_LOCALE
, $source, $match)) {
73 $source = substr(_DIR_IMG
, strlen(_DIR_RACINE
)) . urldecode($match[1]);
75 return @file_exists
($source) ?
$source : false;
78 if (is_null($local)) {
79 $local = fichier_copie_locale($source);
81 if (_DIR_RACINE
and strncmp(_DIR_RACINE
, $local, strlen(_DIR_RACINE
)) == 0) {
82 $local = substr($local, strlen(_DIR_RACINE
));
86 // si $local = '' c'est un fichier refuse par fichier_copie_locale(),
87 // par exemple un fichier qui ne figure pas dans nos documents ;
88 // dans ce cas on n'essaie pas de le telecharger pour ensuite echouer
93 $localrac = _DIR_RACINE
. $local;
94 $t = ($mode == 'force') ?
false : @file_exists
($localrac);
96 // test d'existence du fichier
97 if ($mode == 'test') {
98 return $t ?
$local : '';
101 // sinon voir si on doit/peut le telecharger
102 if ($local == $source or !tester_url_absolue($source)) {
106 if ($mode == 'modif' or !$t) {
107 // passer par un fichier temporaire unique pour gerer les echecs en cours de recuperation
108 // et des eventuelles recuperations concurantes
109 include_spip('inc/acces');
111 $taille_max = _COPIE_LOCALE_MAX_SIZE
;
113 $res = recuperer_url(
115 array('file' => $localrac, 'taille_max' => $taille_max, 'if_modified_since' => $t ?
filemtime($localrac) : '')
117 if (!$res or (!$res['length'] and $res['status'] != 304)) {
118 spip_log("copie_locale : Echec recuperation $source sur $localrac status : " . $res['status'], _LOG_INFO_IMPORTANTE
);
120 if (!$res['length']) {
121 // si $t c'est sans doute juste un not-modified-since
122 return $t ?
$local : false;
124 spip_log("copie_locale : recuperation $source sur $localrac taille " . $res['length'] . ' OK');
126 // pour une eventuelle indexation
131 'operation' => 'copie_locale',
134 'http_res' => $res['length'],
145 * Valider qu'une URL d'un document distant est bien distante
146 * et pas une url localhost qui permet d'avoir des infos sur le serveur
147 * inspiree de https://core.trac.wordpress.org/browser/trunk/src/wp-includes/http.php?rev=36435#L500
150 * @param array $known_hosts
151 * url/hosts externes connus et acceptes
152 * @return false|string
153 * url ou false en cas d'echec
155 function valider_url_distante($url, $known_hosts = array()) {
156 if (!function_exists('protocole_verifier')){
157 include_spip('inc/filtres_mini');
160 if (!protocole_verifier($url, array('http', 'https'))) {
164 $parsed_url = parse_url($url);
165 if (!$parsed_url or empty($parsed_url['host']) ) {
169 if (isset($parsed_url['user']) or isset($parsed_url['pass'])) {
173 if (false !== strpbrk($parsed_url['host'], ':#?[]')) {
177 if (!is_array($known_hosts)) {
178 $known_hosts = array($known_hosts);
180 $known_hosts[] = $GLOBALS['meta']['adresse_site'];
181 $known_hosts[] = url_de_base();
182 $known_hosts = pipeline('declarer_hosts_distants', $known_hosts);
184 $is_known_host = false;
185 foreach ($known_hosts as $known_host) {
186 $parse_known = parse_url($known_host);
188 and strtolower($parse_known['host']) === strtolower($parsed_url['host'])) {
189 $is_known_host = true;
194 if (!$is_known_host) {
195 $host = trim($parsed_url['host'], '.');
196 if (preg_match('#^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $host)) {
199 $ip = gethostbyname($host);
201 // Error condition for gethostbyname()
206 $parts = array_map('intval', explode( '.', $ip ));
207 if (127 === $parts[0] or 10 === $parts[0] or 0 === $parts[0]
208 or ( 172 === $parts[0] and 16 <= $parts[1] and 31 >= $parts[1] )
209 or ( 192 === $parts[0] && 168 === $parts[1] )
216 if (empty($parsed_url['port'])) {
220 $port = $parsed_url['port'];
221 if ($port === 80 or $port === 443 or $port === 8080) {
225 if ($is_known_host) {
226 foreach ($known_hosts as $known_host) {
227 $parse_known = parse_url($known_host);
229 and !empty($parse_known['port'])
230 and strtolower($parse_known['host']) === strtolower($parsed_url['host'])
231 and $parse_known['port'] == $port) {
241 * Preparer les donnes pour un POST
242 * si $donnees est une chaine
243 * - charge a l'envoyeur de la boundariser, de gerer le Content-Type etc...
244 * - on traite les retour ligne pour les mettre au bon format
245 * - on decoupe en entete/corps (separes par ligne vide)
246 * si $donnees est un tableau
247 * - structuration en chaine avec boundary si necessaire ou fournie et bon Content-Type
249 * @param string|array $donnees
250 * @param string $boundary
254 function prepare_donnees_post($donnees, $boundary = '') {
256 // permettre a la fonction qui a demande le post de formater elle meme ses donnees
257 // pour un appel soap par exemple
258 // l'entete est separe des donnees par un double retour a la ligne
259 // on s'occupe ici de passer tous les retours lignes (\r\n, \r ou \n) en \r\n
260 if (is_string($donnees) && strlen($donnees)) {
262 // on repasse tous les \r\n et \r en simples \n
263 $donnees = str_replace("\r\n", "\n", $donnees);
264 $donnees = str_replace("\r", "\n", $donnees);
265 // un double retour a la ligne signifie la fin de l'entete et le debut des donnees
266 $p = strpos($donnees, "\n\n");
268 $entete = str_replace("\n", "\r\n", substr($donnees, 0, $p +
1));
269 $donnees = substr($donnees, $p +
2);
271 $chaine = str_replace("\n", "\r\n", $donnees);
273 /* boundary automatique */
274 // Si on a plus de 500 octects de donnees, on "boundarise"
275 if ($boundary === '') {
277 foreach ($donnees as $cle => $valeur) {
278 if (is_array($valeur)) {
279 foreach ($valeur as $val2) {
280 $taille +
= strlen($val2);
283 // faut-il utiliser spip_strlen() dans inc/charsets ?
284 $taille +
= strlen($valeur);
288 $boundary = substr(md5(rand() . 'spip'), 0, 8);
292 if (is_string($boundary) and strlen($boundary)) {
293 // fabrique une chaine HTTP pour un POST avec boundary
294 $entete = "Content-Type: multipart/form-data; boundary=$boundary\r\n";
296 if (is_array($donnees)) {
297 foreach ($donnees as $cle => $valeur) {
298 if (is_array($valeur)) {
299 foreach ($valeur as $val2) {
300 $chaine .= "\r\n--$boundary\r\n";
301 $chaine .= "Content-Disposition: form-data; name=\"{$cle}[]\"\r\n";
306 $chaine .= "\r\n--$boundary\r\n";
307 $chaine .= "Content-Disposition: form-data; name=\"$cle\"\r\n";
312 $chaine .= "\r\n--$boundary\r\n";
315 // fabrique une chaine HTTP simple pour un POST
316 $entete = 'Content-Type: application/x-www-form-urlencoded' . "\r\n";
318 if (is_array($donnees)) {
319 foreach ($donnees as $cle => $valeur) {
320 if (is_array($valeur)) {
321 foreach ($valeur as $val2) {
322 $chaine[] = rawurlencode($cle) . '[]=' . rawurlencode($val2);
325 $chaine[] = rawurlencode($cle) . '=' . rawurlencode($valeur);
328 $chaine = implode('&', $chaine);
335 return array($entete, $chaine);
339 * Convertir une URL dont le host est en utf8 en ascii
340 * Utilise la librairie https://github.com/phlylabs/idna-convert/tree/v0.9.1
341 * dans sa derniere version compatible toutes version PHP 5
342 * La fonction PHP idn_to_ascii depend d'un package php5-intl et est rarement disponible
344 * @param string $url_idn
345 * @return array|string
347 function url_to_ascii($url_idn) {
349 if ($parts = parse_url($url_idn)) {
350 $host = $parts['host'];
351 if (!preg_match(',^[a-z0-9_\.\-]+$,i', $host)) {
352 include_spip('inc/idna_convert.class');
353 $IDN = new idna_convert();
354 $host_ascii = $IDN->encode($host);
355 $url_idn = explode($host, $url_idn, 2);
356 $url_idn = implode($host_ascii, $url_idn);
364 * Récupère le contenu d'une URL
365 * au besoin encode son contenu dans le charset local
368 * @uses recuperer_entetes()
369 * @uses recuperer_body()
370 * @uses transcoder_page()
373 * @param array $options
374 * bool transcoder : true si on veut transcoder la page dans le charset du site
375 * string methode : Type de requête HTTP à faire (HEAD, GET ou POST)
376 * int taille_max : Arrêter le contenu au-delà (0 = seulement les entetes ==> requête HEAD). Par defaut taille_max = 1Mo ou 16Mo si copie dans un fichier
377 * string|array datas : Pour envoyer des donnees (array) et/ou entetes (string) (force la methode POST si donnees non vide)
378 * string boundary : boundary pour formater les datas au format array
379 * bool refuser_gz : Pour forcer le refus de la compression (cas des serveurs orthographiques)
380 * int if_modified_since : Un timestamp unix pour arrêter la récuperation si la page distante n'a pas été modifiée depuis une date donnée
381 * string uri_referer : Pour préciser un référer différent
382 * string file : nom du fichier dans lequel copier le contenu
383 * int follow_location : nombre de redirections a suivre (0 pour ne rien suivre)
384 * string version_http : version du protocole HTTP a utiliser (par defaut defini par la constante _INC_DISTANT_VERSION_HTTP)
388 * int status : le status de la page
389 * string headers : les entetes de la page
390 * string page : le contenu de la page (vide si copie dans un fichier)
391 * int last_modified : timestamp de derniere modification
392 * string location : url de redirection envoyee par la page
393 * string url : url reelle de la page recuperee
394 * int length : taille du contenu ou du fichier
396 * string file : nom du fichier si enregistre dans un fichier
398 function recuperer_url($url, $options = array()) {
400 'transcoder' => false,
402 'taille_max' => null,
405 'refuser_gz' => false,
406 'if_modified_since' => '',
409 'follow_location' => 10,
410 'version_http' => _INC_DISTANT_VERSION_HTTP
,
412 $options = array_merge($default, $options);
413 // copier directement dans un fichier ?
414 $copy = $options['file'];
416 if ($options['methode'] == 'HEAD') {
417 $options['taille_max'] = 0;
419 if (is_null($options['taille_max'])) {
420 $options['taille_max'] = $copy ? _COPIE_LOCALE_MAX_SIZE
: _INC_DISTANT_MAX_SIZE
;
423 if (!empty($options['datas'])) {
424 list($head, $postdata) = prepare_donnees_post($options['datas'], $options['boundary']);
425 if (stripos($head, 'Content-Length:') === false) {
426 $head .= 'Content-Length: ' . strlen($postdata);
428 $options['datas'] = $head . "\r\n\r\n" . $postdata;
429 if (strlen($postdata)) {
430 $options['methode'] = 'POST';
434 // Accepter les URLs au format feed:// ou qui ont oublie le http:// ou les urls relatives au protocole
435 $url = preg_replace(',^feed://,i', 'http://', $url);
436 if (!tester_url_absolue($url)) {
437 $url = 'http://' . $url;
438 } elseif (strncmp($url, '//', 2) == 0) {
439 $url = 'http:' . $url;
442 $url = url_to_ascii($url);
449 'last_modified' => '',
454 // si on ecrit directement dans un fichier, pour ne pas manipuler en memoire refuser gz
455 $refuser_gz = (($options['refuser_gz'] or $copy) ?
true : false);
457 // ouvrir la connexion et envoyer la requete et ses en-tetes
458 list($handle, $fopen) = init_http(
462 $options['uri_referer'],
464 $options['version_http'],
465 $options['if_modified_since']
468 spip_log("ECHEC init_http $url");
473 // Sauf en fopen, envoyer le flux d'entree
474 // et recuperer les en-tetes de reponses
476 $res = recuperer_entetes_complets($handle, $options['if_modified_since']);
479 $t = @parse_url
($url);
481 // Chinoisierie inexplicable pour contrer
482 // les actions liberticides de l'empire du milieu
483 if (!need_proxy($host)
484 and $res = @file_get_contents
($url)
486 $result['length'] = strlen($res);
488 ecrire_fichier($copy, $res);
489 $result['file'] = $copy;
491 $result['page'] = $res;
499 } elseif ($res['location'] and $options['follow_location']) {
500 $options['follow_location']--;
502 include_spip('inc/filtres');
503 $url = suivre_lien($url, $res['location']);
504 spip_log("recuperer_url recommence sur $url");
506 return recuperer_url($url, $options);
507 } elseif ($res['status'] !== 200) {
508 spip_log('HTTP status ' . $res['status'] . " pour $url");
510 $result['status'] = $res['status'];
511 if (isset($res['headers'])) {
512 $result['headers'] = $res['headers'];
514 if (isset($res['last_modified'])) {
515 $result['last_modified'] = $res['last_modified'];
517 if (isset($res['location'])) {
518 $result['location'] = $res['location'];
522 // on ne veut que les entetes
523 if (!$options['taille_max'] or $options['methode'] == 'HEAD' or $result['status'] == '304') {
528 // s'il faut deballer, le faire via un fichier temporaire
529 // sinon la memoire explose pour les gros flux
532 if (preg_match(",\bContent-Encoding: .*gzip,is", $result['headers'])) {
533 $gz = (_DIR_TMP
. md5(uniqid(mt_rand())) . '.tmp.gz');
536 // si on a pas deja recuperer le contenu par une methode detournee
537 if (!$result['length']) {
538 $res = recuperer_body($handle, $options['taille_max'], $gz ?
$gz : $copy);
541 $result['length'] = $res;
542 $result['file'] = $copy;
544 $result['page'] = &$res;
545 $result['length'] = strlen($result['page']);
547 $result['status'] = 200; // on a reussi, donc !
549 if (!$result['page']) {
553 // Decompresser au besoin
555 $result['page'] = implode('', gzfile($gz));
556 supprimer_fichier($gz);
559 // Faut-il l'importer dans notre charset local ?
560 if ($options['transcoder']) {
561 include_spip('inc/charsets');
562 $result['page'] = transcoder_page($result['page'], $result['headers']);
569 * Récuperer une URL si on l'a pas déjà dans un cache fichier
571 * Le délai de cache est fourni par l'option `delai_cache`
572 * Les autres options et le format de retour sont identiques à la fonction `recuperer_url`
573 * @uses recuperer_url()
576 * @param array $options
577 * int delai_cache : anciennete acceptable pour le contenu (en seconde)
578 * @return array|bool|mixed
580 function recuperer_url_cache($url, $options = array()) {
581 if (!defined('_DELAI_RECUPERER_URL_CACHE')) {
582 define('_DELAI_RECUPERER_URL_CACHE', 3600);
585 'transcoder' => false,
587 'taille_max' => null,
590 'refuser_gz' => false,
591 'if_modified_since' => '',
594 'follow_location' => 10,
595 'version_http' => _INC_DISTANT_VERSION_HTTP
,
596 'delai_cache' => _DELAI_RECUPERER_URL_CACHE
,
598 $options = array_merge($default, $options);
600 // cas ou il n'est pas possible de cacher
601 if (!empty($options['data']) or $options['methode'] == 'POST') {
602 return recuperer_url($url, $options);
605 // ne pas tenter plusieurs fois la meme url en erreur (non cachee donc)
606 static $errors = array();
607 if (isset($errors[$url])) {
608 return $errors[$url];
612 unset($sig['if_modified_since']);
613 unset($sig['delai_cache']);
616 $dir = sous_repertoire(_DIR_CACHE
, 'curl');
617 $cache = md5(serialize($sig)) . '-' . substr(preg_replace(',\W+,', '_', $url), 0, 80);
618 $sub = sous_repertoire($dir, substr($cache, 0, 2));
619 $cache = "$sub$cache";
622 $is_cached = file_exists($cache);
624 and (filemtime($cache) > $_SERVER['REQUEST_TIME'] - $options['delai_cache'])
626 lire_fichier($cache, $res);
627 if ($res = unserialize($res)) {
628 // mettre le last_modified et le status=304 ?
632 $res = recuperer_url($url, $options);
633 // ne pas recharger cette url non cachee dans le meme hit puisque non disponible
636 // on a pas reussi a recuperer mais on avait un cache : l'utiliser
637 lire_fichier($cache, $res);
638 $res = unserialize($res);
641 return $errors[$url] = $res;
643 ecrire_fichier($cache, serialize($res));
650 * Obsolète : Récupère une page sur le net et au besoin l'encode dans le charset local
652 * Gère les redirections de page (301) sur l'URL demandée (maximum 10 redirections)
655 * @uses recuperer_url()
658 * URL de la page à récupérer
659 * @param bool|string $trans
660 * - chaîne longue : c'est un nom de fichier (nom pour sa copie locale)
661 * - true : demande d'encodage/charset
662 * - null : ne retourner que les headers
663 * @param bool $get_headers
664 * Si on veut récupérer les entêtes
665 * @param int|null $taille_max
666 * Arrêter le contenu au-delà (0 = seulement les entetes ==> requête HEAD).
667 * Par defaut taille_max = 1Mo.
668 * @param string|array $datas
669 * Pour faire un POST de données
670 * @param string $boundary
671 * Pour forcer l'envoi par cette méthode
672 * @param bool $refuser_gz
673 * Pour forcer le refus de la compression (cas des serveurs orthographiques)
674 * @param string $date_verif
675 * Un timestamp unix pour arrêter la récuperation si la page distante
676 * n'a pas été modifiée depuis une date donnée
677 * @param string $uri_referer
678 * Pour préciser un référer différent
679 * @return string|bool
680 * - Code de la page obtenue (avec ou sans entête)
681 * - false si la page n'a pu être récupérée (status different de 200)
683 function recuperer_page(
686 $get_headers = false,
694 // $copy = copier le fichier ?
695 $copy = (is_string($trans) and strlen($trans) > 5); // eviter "false" :-)
697 if (!is_null($taille_max) and ($taille_max == 0)) {
704 'transcoder' => $trans === true,
707 'boundary' => $boundary,
708 'refuser_gz' => $refuser_gz,
709 'if_modified_since' => $date_verif,
710 'uri_referer' => $uri_referer,
711 'file' => $copy ?
$trans : '',
712 'follow_location' => 10,
714 if (!is_null($taille_max)) {
715 $options['taille_max'] = $taille_max;
717 // dix tentatives maximum en cas d'entetes 301...
718 $res = recuperer_url($url, $options);
722 if ($res['status'] !== 200) {
726 return $res['headers'] . "\n" . $res['page'];
734 * Obsolete Récupère une page sur le net et au besoin l'encode dans le charset local
738 * @uses recuperer_url()
741 * URL de la page à récupérer
742 * @param bool|null|string $trans
743 * - chaîne longue : c'est un nom de fichier (nom pour sa copie locale)
744 * - true : demande d'encodage/charset
745 * - null : ne retourner que les headers
747 * Type de requête HTTP à faire (HEAD, GET ou POST)
748 * @param int|bool $taille_max
749 * Arrêter le contenu au-delà (0 = seulement les entetes ==> requête HEAD).
750 * Par defaut taille_max = 1Mo.
751 * @param string|array $datas
752 * Pour faire un POST de données
753 * @param bool $refuser_gz
754 * Pour forcer le refus de la compression (cas des serveurs orthographiques)
755 * @param string $date_verif
756 * Un timestamp unix pour arrêter la récuperation si la page distante
757 * n'a pas été modifiée depuis une date donnée
758 * @param string $uri_referer
759 * Pour préciser un référer différent
760 * @return string|array|bool
761 * - Retourne l'URL en cas de 301,
762 * - Un tableau (entête, corps) si ok,
765 function recuperer_lapage(
769 $taille_max = 1048576,
775 // $copy = copier le fichier ?
776 $copy = (is_string($trans) and strlen($trans) > 5); // eviter "false" :-)
778 // si on ecrit directement dans un fichier, pour ne pas manipuler
779 // en memoire refuser gz
785 'transcoder' => $trans === true,
788 'refuser_gz' => $refuser_gz,
789 'if_modified_since' => $date_verif,
790 'uri_referer' => $uri_referer,
791 'file' => $copy ?
$trans : '',
792 'follow_location' => false,
794 if (!is_null($taille_max)) {
795 $options['taille_max'] = $taille_max;
797 // dix tentatives maximum en cas d'entetes 301...
798 $res = recuperer_url($url, $options);
803 if ($res['status'] !== 200) {
807 return array($res['headers'], $res['page']);
811 * Recuperer le contenu sur lequel pointe la resource passee en argument
812 * $taille_max permet de tronquer
813 * de l'url dont on a deja recupere les en-tetes
815 * @param resource $handle
816 * @param int $taille_max
817 * @param string $fichier
818 * fichier dans lequel copier le contenu de la resource
819 * @return bool|int|string
820 * bool false si echec
821 * int taille du fichier si argument fichier fourni
822 * string contenu de la resource
824 function recuperer_body($handle, $taille_max = _INC_DISTANT_MAX_SIZE
, $fichier = '') {
829 include_spip('inc/acces');
830 $tmpfile = "$fichier." . creer_uniqid() . '.tmp';
831 $fp = spip_fopen_lock($tmpfile, 'w', LOCK_EX
);
832 if (!$fp and file_exists($fichier)) {
833 return filesize($fichier);
838 $result = 0; // on renvoie la taille du fichier
840 while (!feof($handle) and $taille < $taille_max) {
841 $res = fread($handle, 16384);
842 $taille +
= strlen($res);
851 spip_fclose_unlock($fp);
852 spip_unlink($fichier);
853 @rename
($tmpfile, $fichier);
854 if (!file_exists($fichier)) {
863 * Lit les entetes de reponse HTTP sur la socket $handle
865 * false en cas d'echec,
866 * un tableau associatif en cas de succes, contenant :
868 * - le tableau complet des headers
869 * - la date de derniere modif si connue
870 * - l'url de redirection si specifiee
872 * @param resource $handle
873 * @param int|bool $if_modified_since
880 function recuperer_entetes_complets($handle, $if_modified_since = false) {
881 $result = array('status' => 0, 'headers' => array(), 'last_modified' => 0, 'location' => '');
883 $s = @trim
(fgets($handle, 16384));
884 if (!preg_match(',^HTTP/[0-9]+\.[0-9]+ ([0-9]+),', $s, $r)) {
887 $result['status'] = intval($r[1]);
888 while ($s = trim(fgets($handle, 16384))) {
889 $result['headers'][] = $s . "\n";
890 preg_match(',^([^:]*): *(.*)$,i', $s, $r);
892 if (strtolower(trim($d)) == 'location' and $result['status'] >= 300 and $result['status'] < 400) {
893 $result['location'] = $v;
894 } elseif ($d == 'Last-Modified') {
895 $result['last_modified'] = strtotime($v);
898 if ($if_modified_since
899 and $result['last_modified']
900 and $if_modified_since > $result['last_modified']
901 and $result['status'] == 200
903 $result['status'] = 304;
906 $result['headers'] = implode('', $result['headers']);
912 * Obsolete : version simplifiee de recuperer_entetes_complets
913 * Retourne les informations d'entête HTTP d'un socket
915 * Lit les entêtes de reponse HTTP sur la socket $f
917 * @uses recuperer_entetes_complets()
921 * Socket d'un fichier (issu de fopen)
922 * @param int|string $date_verif
923 * Pour tester une date de dernière modification
924 * @return string|int|array
925 * - la valeur (chaîne) de l'en-tete Location si on l'a trouvée
926 * - la valeur (numerique) du statut si different de 200, notamment Not-Modified
927 * - le tableau des entetes dans tous les autres cas
929 function recuperer_entetes($f, $date_verif = '') {
930 //Cas ou la page distante n'a pas bouge depuis
932 $res = recuperer_entetes_complets($f, $date_verif);
936 if ($res['location']) {
937 return $res['location'];
939 if ($res['status'] != 200) {
940 return $res['status'];
943 return explode("\n", $res['headers']);
947 * Calcule le nom canonique d'une copie local d'un fichier distant
949 * Si on doit conserver une copie locale des fichiers distants, autant que ca
950 * soit à un endroit canonique
953 * Si ca peut être bijectif c'est encore mieux,
954 * mais là tout de suite je ne trouve pas l'idee, étant donné les limitations
957 * @param string $source
959 * @param string $extension
960 * Extension du fichier
962 * Nom du fichier pour copie locale
964 function nom_fichier_copie_locale($source, $extension) {
965 include_spip('inc/documents');
967 $d = creer_repertoire_documents('distant'); # IMG/distant/
968 $d = sous_repertoire($d, $extension); # IMG/distant/pdf/
970 // on se place tout le temps comme si on etait a la racine
972 $d = preg_replace(',^' . preg_quote(_DIR_RACINE
) . ',', '', $d);
978 . substr(preg_replace(',[^\w-],', '', basename($source)) . '-' . $m, 0, 12)
984 * Donne le nom de la copie locale de la source
986 * Soit obtient l'extension du fichier directement de l'URL de la source,
987 * soit tente de le calculer.
989 * @uses nom_fichier_copie_locale()
990 * @uses recuperer_infos_distantes()
992 * @param string $source
993 * URL de la source distante
995 * Nom du fichier calculé
997 function fichier_copie_locale($source) {
998 // Si c'est deja local pas de souci
999 if (!tester_url_absolue($source)) {
1001 $source = preg_replace(',^' . preg_quote(_DIR_RACINE
) . ',', '', $source);
1007 // optimisation : on regarde si on peut deviner l'extension dans l'url et si le fichier
1008 // a deja ete copie en local avec cette extension
1009 // dans ce cas elle est fiable, pas la peine de requeter en base
1010 $path_parts = pathinfo($source);
1011 if (!isset($path_parts['extension'])) {
1012 $path_parts['extension'] = '';
1014 $ext = $path_parts ?
$path_parts['extension'] : '';
1016 and preg_match(',^\w+$,', $ext) // pas de php?truc=1&...
1017 and $f = nom_fichier_copie_locale($source, $ext)
1018 and file_exists(_DIR_RACINE
. $f)
1024 // Si c'est deja dans la table des documents,
1025 // ramener le nom de sa copie potentielle
1026 $ext = sql_getfetsel('extension', 'spip_documents', 'fichier=' . sql_quote($source) . " AND distant='oui' AND extension <> ''");
1029 return nom_fichier_copie_locale($source, $ext);
1032 // voir si l'extension indiquee dans le nom du fichier est ok
1033 // et si il n'aurait pas deja ete rapatrie
1035 $ext = $path_parts ?
$path_parts['extension'] : '';
1037 if ($ext and sql_getfetsel('extension', 'spip_types_documents', 'extension=' . sql_quote($ext))) {
1038 $f = nom_fichier_copie_locale($source, $ext);
1039 if (file_exists(_DIR_RACINE
. $f)) {
1044 // Ping pour voir si son extension est connue et autorisee
1045 // avec mise en cache du resultat du ping
1047 $cache = sous_repertoire(_DIR_CACHE
, 'rid') . md5($source);
1048 if (!@file_exists
($cache)
1049 or !$path_parts = @unserialize
(spip_file_get_contents($cache))
1050 or _request('var_mode') == 'recalcul'
1052 $path_parts = recuperer_infos_distantes($source, 0, false);
1053 ecrire_fichier($cache, serialize($path_parts));
1055 $ext = !empty($path_parts['extension']) ?
$path_parts['extension'] : '';
1056 if ($ext and sql_getfetsel('extension', 'spip_types_documents', 'extension=' . sql_quote($ext))) {
1057 return nom_fichier_copie_locale($source, $ext);
1059 spip_log("pas de copie locale pour $source");
1064 * Récupérer les infos d'un document distant, sans trop le télécharger
1066 * @param string $source
1069 * Taille maximum du fichier à télécharger
1070 * @param bool $charger_si_petite_image
1071 * Pour télécharger le document s'il est petit
1073 * Couples des informations obtenues parmis :
1076 * - 'type_image' = booleen
1077 * - 'titre' = chaine
1078 * - 'largeur' = intval
1079 * - 'hauteur' = intval
1080 * - 'taille' = intval
1081 * - 'extension' = chaine
1082 * - 'fichier' = chaine
1083 * - 'mime_type' = chaine
1085 function recuperer_infos_distantes($source, $max = 0, $charger_si_petite_image = true) {
1087 // pas la peine de perdre son temps
1088 if (!tester_url_absolue($source)) {
1092 # charger les alias des types mime
1093 include_spip('base/typedoc');
1097 // On va directement charger le debut des images et des fichiers html,
1098 // de maniere a attrapper le maximum d'infos (titre, taille, etc). Si
1099 // ca echoue l'utilisateur devra les entrer...
1100 if ($headers = recuperer_page($source, false, true, $max, '', '', true)) {
1101 list($headers, $a['body']) = preg_split(',\n\n,', $headers, 2);
1103 if (preg_match(",\nContent-Type: *([^[:space:];]*),i", "\n$headers", $regs)) {
1104 $mime_type = (trim($regs[1]));
1109 // Appliquer les alias
1110 while (isset($GLOBALS['mime_alias'][$mime_type])) {
1111 $mime_type = $GLOBALS['mime_alias'][$mime_type];
1114 // Si on a un mime-type insignifiant
1115 // text/plain,application/octet-stream ou vide
1116 // c'est peut-etre que le serveur ne sait pas
1117 // ce qu'il sert ; on va tenter de detecter via l'extension de l'url
1118 // ou le Content-Disposition: attachment; filename=...
1120 if (in_array($mime_type, array('text/plain', '', 'application/octet-stream'))) {
1122 and preg_match(',\.([a-z0-9]+)(\?.*)?$,i', $source, $rext)
1124 $t = sql_fetsel('extension', 'spip_types_documents', 'extension=' . sql_quote($rext[1], '', 'text'));
1127 and preg_match(',^Content-Disposition:\s*attachment;\s*filename=(.*)$,Uims', $headers, $m)
1128 and preg_match(',\.([a-z0-9]+)(\?.*)?$,i', $m[1], $rext)
1130 $t = sql_fetsel('extension', 'spip_types_documents', 'extension=' . sql_quote($rext[1], '', 'text'));
1134 // Autre mime/type (ou text/plain avec fichier d'extension inconnue)
1136 $t = sql_fetsel('extension', 'spip_types_documents', 'mime_type=' . sql_quote($mime_type));
1139 // Toujours rien ? (ex: audio/x-ogg au lieu de application/ogg)
1140 // On essaie de nouveau avec l'extension
1142 and $mime_type != 'text/plain'
1143 and preg_match(',\.([a-z0-9]+)(\?.*)?$,i', $source, $rext)
1145 # eviter xxx.3 => 3gp (> SPIP 3)
1146 $t = sql_fetsel('extension', 'spip_types_documents', 'extension=' . sql_quote($rext[1], '', 'text'));
1150 spip_log("mime-type $mime_type ok, extension " . $t['extension']);
1151 $a['extension'] = $t['extension'];
1153 # par defaut on retombe sur '.bin' si c'est autorise
1154 spip_log("mime-type $mime_type inconnu");
1155 $t = sql_fetsel('extension', 'spip_types_documents', "extension='bin'");
1159 $a['extension'] = $t['extension'];
1162 if (preg_match(",\nContent-Length: *([^[:space:]]*),i", "\n$headers", $regs)) {
1163 $a['taille'] = intval($regs[1]);
1167 // Echec avec HEAD, on tente avec GET
1168 if (!$a and !$max) {
1169 spip_log("tenter GET $source");
1170 $a = recuperer_infos_distantes($source, _INC_DISTANT_MAX_SIZE
);
1173 // si on a rien trouve pas la peine d'insister
1178 // S'il s'agit d'une image pas trop grosse ou d'un fichier html, on va aller
1179 // recharger le document en GET et recuperer des donnees supplementaires...
1180 if (preg_match(',^image/(jpeg|gif|png|swf),', $mime_type)) {
1182 and (empty($a['taille']) or $a['taille'] < _INC_DISTANT_MAX_SIZE
)
1183 and isset($GLOBALS['meta']['formats_graphiques'])
1184 and (strpos($GLOBALS['meta']['formats_graphiques'], $a['extension']) !== false)
1185 and $charger_si_petite_image
1187 $a = recuperer_infos_distantes($source, _INC_DISTANT_MAX_SIZE
);
1190 $a['fichier'] = _DIR_RACINE
. nom_fichier_copie_locale($source, $a['extension']);
1191 ecrire_fichier($a['fichier'], $a['body']);
1192 $size_image = @getimagesize
($a['fichier']);
1193 $a['largeur'] = intval($size_image[0]);
1194 $a['hauteur'] = intval($size_image[1]);
1195 $a['type_image'] = true;
1200 // Fichier swf, si on n'a pas la taille, on va mettre 425x350 par defaut
1201 // ce sera mieux que 0x0
1202 if ($a and isset($a['extension']) and $a['extension'] == 'swf'
1203 and empty($a['largeur'])
1205 $a['largeur'] = 425;
1206 $a['hauteur'] = 350;
1209 if ($mime_type == 'text/html') {
1210 include_spip('inc/filtres');
1211 $page = recuperer_page($source, true, false, _INC_DISTANT_MAX_SIZE
);
1212 if (preg_match(',<title>(.*?)</title>,ims', $page, $regs)) {
1213 $a['titre'] = corriger_caracteres(trim($regs[1]));
1215 if (!isset($a['taille']) or !$a['taille']) {
1216 $a['taille'] = strlen($page); # a peu pres
1219 $a['mime_type'] = $mime_type;
1226 * Tester si un host peut etre recuperer directement ou doit passer par un proxy
1228 * On peut passer en parametre le proxy et la liste des host exclus,
1229 * pour les besoins des tests, lors de la configuration
1231 * @param string $host
1232 * @param string $http_proxy
1233 * @param string $http_noproxy
1236 function need_proxy($host, $http_proxy = null, $http_noproxy = null) {
1237 if (is_null($http_proxy)) {
1238 $http_proxy = isset($GLOBALS['meta']['http_proxy']) ?
$GLOBALS['meta']['http_proxy'] : null;
1240 // rien a faire si pas de proxy :)
1241 if (is_null($http_proxy) or !$http_proxy = trim($http_proxy)) {
1245 if (is_null($http_noproxy)) {
1246 $http_noproxy = isset($GLOBALS['meta']['http_noproxy']) ?
$GLOBALS['meta']['http_noproxy'] : null;
1248 // si pas d'exception, on retourne le proxy
1249 if (is_null($http_noproxy) or !$http_noproxy = trim($http_noproxy)) {
1253 // si le host ou l'un des domaines parents est dans $http_noproxy on fait exception
1254 // $http_noproxy peut contenir plusieurs domaines separes par des espaces ou retour ligne
1255 $http_noproxy = str_replace("\n", " ", $http_noproxy);
1256 $http_noproxy = str_replace("\r", " ", $http_noproxy);
1257 $http_noproxy = " $http_noproxy ";
1259 // si le domaine exact www.example.org est dans les exceptions
1260 if (strpos($http_noproxy, " $domain ") !== false)
1263 while (strpos($domain, '.') !== false) {
1264 $domain = explode('.', $domain);
1265 array_shift($domain);
1266 $domain = implode('.', $domain);
1268 // ou si un domaine parent commencant par un . est dans les exceptions (indiquant qu'il couvre tous les sous-domaines)
1269 if (strpos($http_noproxy, " .$domain ") !== false) {
1274 // ok c'est pas une exception
1280 * Initialise une requete HTTP avec entetes
1282 * Décompose l'url en son schema+host+path+port et lance la requete.
1283 * Retourne le descripteur sur lequel lire la réponse.
1285 * @uses lance_requete()
1287 * @param string $method
1289 * @param string $url
1290 * @param bool $refuse_gz
1291 * @param string $referer
1292 * @param string $datas
1293 * @param string $vers
1294 * @param string $date
1297 function init_http($method, $url, $refuse_gz = false, $referer = '', $datas = '', $vers = 'HTTP/1.0', $date = '') {
1298 $user = $via_proxy = $proxy_user = '';
1301 $t = @parse_url
($url);
1303 if ($t['scheme'] == 'http') {
1306 } elseif ($t['scheme'] == 'https') {
1308 $noproxy = 'ssl://';
1309 if (!isset($t['port']) ||
!($port = $t['port'])) {
1313 $scheme = $t['scheme'];
1314 $noproxy = $scheme . '://';
1316 if (isset($t['user'])) {
1317 $user = array($t['user'], $t['pass']);
1320 if (!isset($t['port']) ||
!($port = $t['port'])) {
1323 if (!isset($t['path']) ||
!($path = $t['path'])) {
1327 if (!empty($t['query'])) {
1328 $path .= '?' . $t['query'];
1331 $f = lance_requete($method, $scheme, $user, $host, $path, $port, $noproxy, $refuse_gz, $referer, $datas, $vers, $date);
1332 if (!$f or !is_resource($f)) {
1333 // fallback : fopen si on a pas fait timeout dans lance_requete
1334 // ce qui correspond a $f===110
1336 and !need_proxy($host)
1337 and !_request('tester_proxy')
1338 and (!isset($GLOBALS['inc_distant_allow_fopen']) or $GLOBALS['inc_distant_allow_fopen'])
1340 $f = @fopen
($url, 'rb');
1341 spip_log("connexion vers $url par simple fopen");
1349 return array($f, $fopen);
1353 * Lancer la requete proprement dite
1355 * @param string $method
1356 * type de la requete (GET, HEAD, POST...)
1357 * @param string $scheme
1358 * protocole (http, tls, ftp...)
1359 * @param array $user
1360 * couple (utilisateur, mot de passe) en cas d'authentification http
1361 * @param string $host
1363 * @param string $path
1364 * chemin de la page cherchee
1365 * @param string $port
1366 * port utilise pour la connexion
1367 * @param bool $noproxy
1368 * protocole utilise si requete sans proxy
1369 * @param bool $refuse_gz
1370 * refuser la compression GZ
1371 * @param string $referer
1373 * @param string $datas
1375 * @param string $vers
1377 * @param int|string $date
1378 * timestamp pour entente If-Modified-Since
1379 * @return bool|resource
1380 * false|int si echec
1381 * resource socket vers l'url demandee
1383 function lance_requete(
1399 $http_proxy = need_proxy($host);
1401 $user = urlencode($user[0]) . ':' . urlencode($user[1]);
1406 if (defined('_PROXY_HTTPS_VIA_CONNECT') and in_array($scheme , array('tls','ssl'))) {
1407 $path_host = (!$user ?
'' : "$user@") . $host . (($port != 80) ?
":$port" : '');
1408 $connect = 'CONNECT ' . $path_host . " $vers\r\n"
1409 . "Host: $path_host\r\n"
1410 . "Proxy-Connection: Keep-Alive\r\n";
1412 $path = (in_array($scheme , array('tls','ssl')) ?
'https://' : "$scheme://")
1413 . (!$user ?
'' : "$user@")
1414 . "$host" . (($port != 80) ?
":$port" : '') . $path;
1416 $t2 = @parse_url
($http_proxy);
1417 $first_host = $t2['host'];
1418 if (!($port = $t2['port'])) {
1422 $proxy_user = base64_encode($t2['user'] . ':' . $t2['pass']);
1425 $first_host = $noproxy . $host;
1429 $streamContext = stream_context_create(array(
1431 'verify_peer' => false,
1432 'allow_self_signed' => true,
1433 'SNI_enabled' => true,
1434 'peer_name' => $host,
1437 if (version_compare(phpversion(), '5.6', '<')) {
1438 stream_context_set_option($streamContext, 'ssl', 'SNI_server_name', $host);
1440 $f = @stream_socket_client
(
1441 "tcp://$first_host:$port",
1444 _INC_DISTANT_CONNECT_TIMEOUT
,
1445 STREAM_CLIENT_CONNECT
,
1448 spip_log("Recuperer $path sur $first_host:$port par $f (via CONNECT)", 'connect');
1450 spip_log("Erreur connexion $errno $errstr", _LOG_ERREUR
);
1453 stream_set_timeout($f, _INC_DISTANT_CONNECT_TIMEOUT
);
1455 fputs($f, $connect);
1457 $res = fread($f, 1024);
1459 or !count($res = explode(' ', $res))
1460 or $res[1] !== '200'
1462 spip_log("Echec CONNECT sur $first_host:$port", 'connect' . _LOG_INFO_IMPORTANTE
);
1467 // important, car sinon on lit trop vite et les donnees ne sont pas encore dispo
1468 stream_set_blocking($f, true);
1469 // envoyer le handshake
1470 stream_socket_enable_crypto($f, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT
);
1471 spip_log("OK CONNECT sur $first_host:$port", 'connect');
1475 $f = @fsockopen
($first_host, $port, $errno, $errstr, _INC_DISTANT_CONNECT_TIMEOUT
);
1476 } while (!$f and $ntry-- and $errno !== 110 and sleep(1));
1477 spip_log("Recuperer $path sur $first_host:$port par $f");
1479 spip_log("Erreur connexion $errno $errstr", _LOG_ERREUR
);
1483 stream_set_timeout($f, _INC_DISTANT_CONNECT_TIMEOUT
);
1486 $site = isset($GLOBALS['meta']['adresse_site']) ?
$GLOBALS['meta']['adresse_site'] : '';
1489 if ($port != (in_array($scheme , array('tls','ssl')) ?
443 : 80)) {
1490 $host_port .= ":$port";
1492 $req = "$method $path $vers\r\n"
1493 . "Host: $host_port\r\n"
1494 . 'User-Agent: ' . _INC_DISTANT_USER_AGENT
. "\r\n"
1495 . ($refuse_gz ?
'' : ('Accept-Encoding: ' . _INC_DISTANT_CONTENT_ENCODING
. "\r\n"))
1496 . (!$site ?
'' : "Referer: $site/$referer\r\n")
1497 . (!$date ?
'' : 'If-Modified-Since: ' . (gmdate('D, d M Y H:i:s', $date) . " GMT\r\n"))
1498 . (!$user ?
'' : ('Authorization: Basic ' . base64_encode($user) . "\r\n"))
1499 . (!$proxy_user ?
'' : "Proxy-Authorization: Basic $proxy_user\r\n")
1500 . (!strpos($vers, '1.1') ?
'' : "Keep-Alive: 300\r\nConnection: keep-alive\r\n");
1502 # spip_log("Requete\n$req");
1504 fputs($f, $datas ?
$datas : "\r\n");