Subversion Repositories Applications.framework

Rev

Rev 85 | Rev 120 | Go to most recent revision | 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
* 
* PHP Version 5 
* 
* @category  Class
* @package   Framework
// auteur principal
* @author    Christian Schmidt <schmidt@php.net>
// autre auteurs
* @author    aurelien <aurelien@tela-botanica.org>
* @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: Url.php 105 2009-08-31 15:25:05Z aurelien $ 
* @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,
                         ini_get('arg_separator.input'));
        $this->setOption(self::OPTION_SEPARATEUR_SORTIE,
                         ini_get('arg_separator.output'));
        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);
    }

    /**
     * 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();
    }
}