// Autre auteurs * @author Aurélien PERONNET * @author Jean-Pascal MILCENT * @copyright 2009 Tela-Botanica * @license http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL * @license http://www.gnu.org/licenses/gpl.html Licence GNU-GPL * @version SVN: $Id$ * @link /doc/framework/ * */ class Url { /** * Parsing strict dans resoudre() (voir RFC 3986, section 5.2.2). Par défaut * à true. */ const OPTION_STRICTE = 'strict'; /** * Répresenter les tableaux dans les requêtes en utilisant la notation php []. Par défaut à true. */ const OPTION_UTILISER_CROCHETS = 'use_brackets'; /** * URL-encoder les clés des variables dans les requêtes. Par défaut à true. */ const OPTION_ENCODER_CLES = 'encode_keys'; /** * Séparateurs de variables lors du parsing de la requête. Chaque caractère * est considéré comme un séparateur. Par défaut, spécifié par le paramêtre * arg_separator.input dans php.ini (par défaut "&"). */ const OPTION_SEPARATEUR_ENTREE = 'input_separator'; /** * Séparateur de variables lors de la génération de la requête. Par défaut, spécifié * par le paramètre arg_separator.output dans php.ini (par défaut "&"). */ const OPTION_SEPARATEUR_SORTIE = 'output_separator'; /** * Options par défaut correspondant au comportement de php * vis à vis de $_GET */ private $options = array( self::OPTION_STRICTE => true, self::OPTION_UTILISER_CROCHETS => true, self::OPTION_ENCODER_CLES => true, self::OPTION_SEPARATEUR_ENTREE => 'x&', self::OPTION_SEPARATEUR_SORTIE => 'x&'); /** * @var string|bool */ private $schema = false; /** * @var string|bool */ private $infoUtilisateur = false; /** * @var string|bool */ private $hote = false; /** * @var int|bool */ private $port = false; /** * @var string */ private $chemin = ''; /** * @var string|bool */ private $requete = false; /** * @var string|bool */ private $fragment = false; /** * @param string $url une URL relative ou absolue * @param array $options */ public function __construct($url, $options = null) { $this->setOption(self::OPTION_SEPARATEUR_ENTREE, Config::get('fw_url_arg_separateur_entree')); $this->setOption(self::OPTION_SEPARATEUR_SORTIE, Config::get('fw_url_arg_separateur_sortie')); if (is_array($options)) { foreach ($options as $nomOption => $valeur) { $this->setOption($nomOption); } } if (preg_match('@^([a-z][a-z0-9.+-]*):@i', $url, $reg)) { $this->schema = $reg[1]; $url = substr($url, strlen($reg[0])); } if (preg_match('@^//([^/#?]+)@', $url, $reg)) { $this->setAutorite($reg[1]); $url = substr($url, strlen($reg[0])); } $i = strcspn($url, '?#'); $this->chemin = substr($url, 0, $i); $url = substr($url, $i); if (preg_match('@^\?([^#]*)@', $url, $reg)) { $this->requete = $reg[1]; $url = substr($url, strlen($reg[0])); } if ($url) { $this->fragment = substr($url, 1); } } /** * Retourne le schéma, c.a.d. "http" ou "urn", ou false si aucun schéma n'est * spécifié, i.e. l'url est une url relative * * @return string|bool */ public function getSchema() { return $this->schema; } /** * @param string|bool $schema * * @return void * @see getSchema() */ public function setSchema($schema) { $this->schema = $schema; } /** * renvoie la partie user de la partie infoUtilisateur (partie précédant le premier * ":"), ou false si aucune partie infoUtilisateur n'est définie. * * @return string|bool */ public function getUtilisateur() { return $this->infoUtilisateur !== false ? preg_replace('@:.*$@', '', $this->infoUtilisateur) : false; } /** * renvoie la partie mot de passe de la partie infoUtilisateur (partie après le premier * ":"), , ou false si aucune partie infoUtilisateur n'est définie (i.e. l'URL ne contient * pas de "@" en face du nom d'hôte) ou si la partie infoUtilisateur ne contient pas de ":". * * @return string|bool */ public function getMotDePasse() { return $this->infoUtilisateur !== false ? substr(strstr($this->infoUtilisateur, ':'), 1) : false; } /** * Renvoie la partie userinfio, ou false si celle-ci n'existe pas, i.e. si la partie * autorité ne contient pas de "@" * * @return string|bool */ public function getInfoUtilisateur() { return $this->infoUtilisateur; } /** * Setteur pour la partie infoUtilisateur. Si deux argument sont passé, ils sont combinés * dans la partie infoUtilisateur de cette manière username ":" password. * * @param string|bool $infoUtilisateur infoUtilisateur ou username * @param string|bool $motDePasse * * @return void */ public function setInfoUtilisateur($infoUtilisateur, $motDePasse = false) { $this->infoUtilisateur = $infoUtilisateur; if ($motDePasse !== false) { $this->infoUtilisateur .= ':' . $motDePasse; } } /** * Renvoie la partie hôte, ou false s'il n'y a pas de partie autorité, c.a.d. * l'URL est relative. * * @return string|bool */ public function getHote() { return $this->hote; } /** * @param string|bool $hote * * @return void */ public function setHote($hote) { $this->hote = $hote; } /** * Renvoie le numéro de port, ou false si aucun numéro de port n'est spécifié, * i.e. le port par défaut doit utilisé. * * @return int|bool */ public function getPort() { return $this->port; } /** * @param int|bool $port * * @return void */ public function setPort($port) { $this->port = intval($port); } /** * Renvoie la partie autorité, i.e. [ infoUtilisateur "@" ] hote [ ":" port ], ou * false si celle-ci est absente. * * @return string|bool */ public function getAutorite() { if (!$this->hote) { return false; } $autorite = ''; if ($this->infoUtilisateur !== false) { $autorite .= $this->infoUtilisateur . '@'; } $autorite .= $this->hote; if ($this->port !== false) { $autorite .= ':' . $this->port; } return $autorite; } /** * @param string|false $autorite * * @return void */ public function setAutorite($autorite) { $this->user = false; $this->pass = false; $this->hote = false; $this->port = false; if (preg_match('@^(([^\@]+)\@)?([^:]+)(:(\d*))?$@', $autorite, $reg)) { if ($reg[1]) { $this->infoUtilisateur = $reg[2]; } $this->hote = $reg[3]; if (isset($reg[5])) { $this->port = intval($reg[5]); } } } /** * Renvoie la partie chemin (chemin) (éventuellement vide). * * @return string */ public function getChemin() { return $this->chemin; } /** * @param string $chemin * * @return void */ public function setChemin($chemin) { $this->chemin = $chemin; } /** * renvoie la chaine de requête (requete string) (sans le premier "?"), ou false si "?" * n'est pas présent dans l'url. * * @return string|bool * @see self::getVariablesRequete() */ public function getRequete() { return $this->requete; } /** * @param string|bool $requete * * @return void * @see self::setVariablesRequete() */ public function setRequete($requete) { $this->requete = $requete; } /** * Renvoie le nom du fragment, ou false si "#" n'est pas present dans l'URL. * * @return string|bool */ public function getFragment() { return $this->fragment; } /** * @param string|bool $fragment * * @return void */ public function setFragment($fragment) { $this->fragment = $fragment; } /** * Renvoie la requete string sous forme d'un tableau de variables telles qu'elles apparaitraient * dans le $_GET d'un script PHP * * @return array */ public function getVariablesRequete() { $pattern = '/' . preg_quote($this->getOption(self::OPTION_SEPARATEUR_ENTREE), '/') . '/'; $parties = preg_split($pattern, $this->requete, -1, PREG_SPLIT_NO_EMPTY); $retour = array(); foreach ($parties as $partie) { if (strpos($partie, '=') !== false) { list($cle, $valeur) = explode('=', $partie, 2); } else { $cle = $partie; $valeur = null; } if ($this->getOption(self::OPTION_ENCODER_CLES)) { $cle = rawurldecode($cle); } $valeur = rawurldecode($valeur); if ($this->getOption(self::OPTION_UTILISER_CROCHETS) && preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $cle, $matches)) { $cle = $matches[1]; $idx = $matches[2]; // On s'assure que c'est bien un tableau if (empty($retour[$cle]) || !is_array($retour[$cle])) { $retour[$cle] = array(); } // Ajout des données if ($idx === '') { $retour[$cle][] = $valeur; } else { $retour[$cle][$idx] = $valeur; } } elseif (!$this->getOption(self::OPTION_UTILISER_CROCHETS) && !empty($retour[$cle]) ) { $retour[$cle] = (array) $retour[$cle]; $retour[$cle][] = $valeur; } else { $retour[$cle] = $valeur; } } return $retour; } /** * @param array $tableau (nom => valeur) tableau * * @return void */ public function setVariablesRequete(array $tableau) { if (!$tableau) { $this->requete = false; } else { foreach ($tableau as $nom => $valeur) { if ($this->getOption(self::OPTION_ENCODER_CLES)) { $nom = rawurlencode($nom); } if (is_array($valeur)) { foreach ($valeur as $k => $v) { $parties[] = $this->getOption(self::OPTION_UTILISER_CROCHETS) ? sprintf('%s[%s]=%s', $nom, $k, $v) : ($nom . '=' . $v); } } elseif (!is_null($valeur)) { $parties[] = $nom . '=' . $valeur; } else { $parties[] = $nom; } } $this->requete = implode($this->getOption(self::OPTION_SEPARATEUR_SORTIE), $parties); } } /** * @param string $nom * @param mixed $valeur * * @return array */ public function setVariableRequete($nom, $valeur) { $tableau = $this->getVariablesRequete(); $tableau[$nom] = $valeur; $this->setVariablesRequete($tableau); } /** * @param string $nom * * @return void */ public function unsetVariableRequete($nom) { $tableau = $this->getVariablesRequete(); unset($tableau[$nom]); $this->setVariablesRequete($tableau); } /** * @param array $noms tableau des noms de variable à supprimer de l'url. * * @return void */ public function unsetVariablesRequete($noms) { $tableau = $this->getVariablesRequete(); foreach ($noms as $nom) { unset($tableau[$nom]); } $this->setVariablesRequete($tableau); } /** * Renvoie un représentation sous forme de chaine de l'URL * * @return string */ public function getURL() { // Voir RFC 3986, section 5.3 $url = ""; if ($this->schema !== false) { $url .= $this->schema . ':'; } $autorite = $this->getAutorite(); if ($autorite !== false) { $url .= '//' . $autorite; } $url .= $this->chemin; if ($this->requete !== false) { $url .= '?' . $this->requete; } if ($this->fragment !== false) { $url .= '#' . $this->fragment; } return $url; } /** * Renvoie une représentation de cette URL sous forme de chaine normalisée. Utile pour la * comparaison d'URLs * * @return string */ public function getURLNormalisee() { $url = clone $this; $url->normaliser(); return $url->getUrl(); } /** * Renvoie une instance normalisée de Url * * @return Url */ public function normaliser() { // See RFC 3886, section 6 // les cchémas sont insesibles à la casse if ($this->schema) { $this->schema = strtolower($this->schema); } // les noms d'hotes sont insensibles à la casse if ($this->hote) { $this->hote = strtolower($this->hote); } // Supprimer le numéro de port par défaut pour les schemas connus (RFC 3986, section 6.2.3) if ($this->port && $this->schema && $this->port == getservbyname($this->schema, 'tcp')) { $this->port = false; } // normalisation dans le cas d'un encodage avec %XX pourcentage (RFC 3986, section 6.2.2.1) foreach (array('infoUtilisateur', 'hote', 'chemin') as $partie) { if ($this->$partie) { $this->$partie = preg_replace('/%[0-9a-f]{2}/ie', 'strtoupper("\0")', $this->$partie); } } // normalisation des segments du chemin (RFC 3986, section 6.2.2.3) $this->chemin = self::supprimerSegmentsAPoints($this->chemin); // normalisation basée sur le schéma (RFC 3986, section 6.2.3) if ($this->hote && !$this->chemin) { $this->chemin = '/'; } } /** * Renvoie vrai ou faux suivant que l'instance en cours représente une URL relative ou absolue. * * @return bool */ public function etreAbsolue() { return (bool) $this->schema; } /** * Renvoie une instance de Url représentant une URL absolue relative à * cette URL. * * @param Url|string $reference URL relative * * @return Url */ public function resoudre($reference) { if (is_string($reference)) { $reference = new self($reference); } if (!$this->etreAbsolue()) { throw new Exception('L\'URL de base doit être absolue !'); } // Un parseur non strict peut choisir d'ignorer un schema dans la référence // si celui ci est identique au schéma de base de l'URI. if (!$this->getOption(self::OPTION_STRICTE) && $reference->schema == $this->schema) { $reference->schema = false; } $cible = new self(''); if ($reference->schema !== false) { $cible->schema = $reference->schema; $cible->setAutorite($reference->getAutorite()); $cible->chemin = self::supprimerSegmentsAPoints($reference->chemin); $cible->requete = $reference->requete; } else { $autorite = $reference->getAutorite(); if ($autorite !== false) { $cible->setAutorite($autorite); $cible->chemin = self::supprimerSegmentsAPoints($reference->chemin); $cible->requete = $reference->requete; } else { if ($reference->chemin == '') { $cible->chemin = $this->chemin; if ($reference->requete !== false) { $cible->requete = $reference->requete; } else { $cible->requete = $this->requete; } } else { if (substr($reference->chemin, 0, 1) == '/') { $cible->chemin = self::supprimerSegmentsAPoints($reference->chemin); } else { // Concaténation chemins (RFC 3986, section 5.2.3) if ($this->hote !== false && $this->chemin == '') { $cible->chemin = '/' . $this->chemin; } else { $i = strrpos($this->chemin, '/'); if ($i !== false) { $cible->chemin = substr($this->chemin, 0, $i + 1); } $cible->chemin .= $reference->chemin; } $cible->chemin = self::supprimerSegmentsAPoints($cible->chemin); } $cible->requete = $reference->requete; } $cible->setAutorite($this->getAutorite()); } $cible->schema = $this->schema; } $cible->fragment = $reference->fragment; return $cible; } /** * La suppression des segments à points est décrite dans la RFC 3986, section 5.2.4, e.g. * "/foo/../bar/baz" => "/bar/baz" * * @param string $chemin un chemin * * @return string un chemin */ private static function supprimerSegmentsAPoints($chemin) { $sortie = ''; // Assurons de ne pas nous retrouver piégés dans une boucle infinie due à un bug de // cette méthode $j = 0; while ($chemin && $j++ < 100) { // Étape A if (substr($chemin, 0, 2) == './') { $chemin = substr($chemin, 2); } elseif (substr($chemin, 0, 3) == '../') { $chemin = substr($chemin, 3); // Étape B } elseif (substr($chemin, 0, 3) == '/./' || $chemin == '/.') { $chemin = '/' . substr($chemin, 3); // Étape C } elseif (substr($chemin, 0, 4) == '/../' || $chemin == '/..') { $chemin = '/' . substr($chemin, 4); $i = strrpos($sortie, '/'); $sortie = $i === false ? '' : substr($sortie, 0, $i); // Étape D } elseif ($chemin == '.' || $chemin == '..') { $chemin = ''; // Étape E } else { $i = strpos($chemin, '/'); if ($i === 0) { $i = strpos($chemin, '/', 1); } if ($i === false) { $i = strlen($chemin); } $sortie .= substr($chemin, 0, $i); $chemin = substr($chemin, $i); } } return $sortie; } /** * Renvoie une instance de Url representant l'URL canonique du script PHP * en cours d'éxécution * * @return string */ public static function getCanonique() { if (!isset($_SERVER['REQUEST_METHOD'])) { // ALERT - pas d'URL en cours throw new Exception('Le script n\'a pas été appellé à travers un serveur web'); } // on part d'une URL relative $url = new self($_SERVER['PHP_SELF']); $url->schema = isset($_SERVER['HTTPS']) ? 'https' : 'http'; $url->hote = $_SERVER['SERVER_NAME']; $port = intval($_SERVER['SERVER_PORT']); if ($url->schema == 'http' && $port != 80 || $url->schema == 'https' && $port != 443) { $url->port = $port; } return $url; } /** * Renvoie l'URL utilisée pour récupérer la requête en cours * * @return string */ public static function getURLDemande() { return self::getDemande()->getUrl(); } /** * Renvoie une instance de Url representant l'URL utilisée pour * récupérer la requête en cours * * @return Url */ public static function getDemande() { if (!isset($_SERVER['REQUEST_METHOD'])) { // ALERTE - pas d'URL en cours throw new Exception('Le script n\'a pas été appellé à travers un serveur web'); } // On part d'une URL relative $url = new self($_SERVER['REQUEST_URI']); $url->schema = isset($_SERVER['HTTPS']) ? 'https' : 'http'; // On met à jour les valeurs de l'hote et si possible du port $url->setAutorite($_SERVER['HTTP_hote']); return $url; } /** * Met à jour la valeur de l'option spécifiée. * * @param string $nomOption une des constantes commençant par self::OPTION_ * @param mixed $valeur valeur de l'option * * @return void * @see self::OPTION_STRICTE * @see self::OPTION_UTILISER_CROCHETS * @see self::OPTION_ENCODER_CLES */ function setOption($nomOption, $valeur) { if (!array_key_exists($nomOption, $this->options)) { return false; } $this->options[$nomOption] = $valeur; } /** * Renvoie la valeur de l'option specifiée. * * @param string $nomOption Nom de l'option demandée * * @return mixed */ function getOption($nomOption) { return isset($this->options[$nomOption]) ? $this->options[$nomOption] : false; } public function __toString() { return $this->getURL(); } }