Subversion Repositories Applications.framework

Rev

Rev 467 | Blame | Compare with Previous | Last modification | View Log | RSS feed

<?php
// declare(encoding='UTF-8');
/**
 * classe Url, gérant le découpage des paramètres, leurs modification etc...
 * Traduction et conversion d'une classe (NET_Url2) issue de Pear
 *
 * @category    PHP 5.2
 * @package             Framework
 * @author              Christian SCHMIDT<schmidt@php.net>
 * @author              Aurélien PERONNET <aurelien@tela-botanica.org>
 * @author              Jean-Pascal MILCENT <jpm@tela-botanica.org>
 * @copyright   Copyright (c) 2009, Tela Botanica (accueil@tela-botanica.org)
 * @license             GNU-GPL-v3 <http://www.gnu.org/licenses/gpl.html>
 * @license             CECILL-v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt>
 */
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();
        }
}