home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-includes / Requests / IRI.php < prev    next >
Encoding:
PHP Script  |  2016-10-04  |  27.8 KB  |  1,085 lines

  1. <?php
  2. /**
  3.  * IRI parser/serialiser/normaliser
  4.  *
  5.  * @package Requests
  6.  * @subpackage Utilities
  7.  */
  8.  
  9. /**
  10.  * IRI parser/serialiser/normaliser
  11.  *
  12.  * Copyright (c) 2007-2010, Geoffrey Sneddon and Steve Minutillo.
  13.  * All rights reserved.
  14.  *
  15.  * Redistribution and use in source and binary forms, with or without
  16.  * modification, are permitted provided that the following conditions are met:
  17.  *
  18.  *  * Redistributions of source code must retain the above copyright notice,
  19.  *       this list of conditions and the following disclaimer.
  20.  *
  21.  *  * Redistributions in binary form must reproduce the above copyright notice,
  22.  *       this list of conditions and the following disclaimer in the documentation
  23.  *       and/or other materials provided with the distribution.
  24.  *
  25.  *  * Neither the name of the SimplePie Team nor the names of its contributors
  26.  *       may be used to endorse or promote products derived from this software
  27.  *       without specific prior written permission.
  28.  *
  29.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  30.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  31.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  32.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
  33.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  34.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  35.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  36.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  37.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  38.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  39.  * POSSIBILITY OF SUCH DAMAGE.
  40.  *
  41.  * @package Requests
  42.  * @subpackage Utilities
  43.  * @author Geoffrey Sneddon
  44.  * @author Steve Minutillo
  45.  * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo
  46.  * @license http://www.opensource.org/licenses/bsd-license.php
  47.  * @link http://hg.gsnedders.com/iri/
  48.  *
  49.  * @property string $iri IRI we're working with
  50.  * @property-read string $uri IRI in URI form, {@see to_uri}
  51.  * @property string $scheme Scheme part of the IRI
  52.  * @property string $authority Authority part, formatted for a URI (userinfo + host + port)
  53.  * @property string $iauthority Authority part of the IRI (userinfo + host + port)
  54.  * @property string $userinfo Userinfo part, formatted for a URI (after '://' and before '@')
  55.  * @property string $iuserinfo Userinfo part of the IRI (after '://' and before '@')
  56.  * @property string $host Host part, formatted for a URI
  57.  * @property string $ihost Host part of the IRI
  58.  * @property string $port Port part of the IRI (after ':')
  59.  * @property string $path Path part, formatted for a URI (after first '/')
  60.  * @property string $ipath Path part of the IRI (after first '/')
  61.  * @property string $query Query part, formatted for a URI (after '?')
  62.  * @property string $iquery Query part of the IRI (after '?')
  63.  * @property string $fragment Fragment, formatted for a URI (after '#')
  64.  * @property string $ifragment Fragment part of the IRI (after '#')
  65.  */
  66. class Requests_IRI {
  67.     /**
  68.      * Scheme
  69.      *
  70.      * @var string
  71.      */
  72.     protected $scheme = null;
  73.  
  74.     /**
  75.      * User Information
  76.      *
  77.      * @var string
  78.      */
  79.     protected $iuserinfo = null;
  80.  
  81.     /**
  82.      * ihost
  83.      *
  84.      * @var string
  85.      */
  86.     protected $ihost = null;
  87.  
  88.     /**
  89.      * Port
  90.      *
  91.      * @var string
  92.      */
  93.     protected $port = null;
  94.  
  95.     /**
  96.      * ipath
  97.      *
  98.      * @var string
  99.      */
  100.     protected $ipath = '';
  101.  
  102.     /**
  103.      * iquery
  104.      *
  105.      * @var string
  106.      */
  107.     protected $iquery = null;
  108.  
  109.     /**
  110.      * ifragment
  111.      *
  112.      * @var string
  113.      */
  114.     protected $ifragment = null;
  115.  
  116.     /**
  117.      * Normalization database
  118.      *
  119.      * Each key is the scheme, each value is an array with each key as the IRI
  120.      * part and value as the default value for that part.
  121.      */
  122.     protected $normalization = array(
  123.         'acap' => array(
  124.             'port' => 674
  125.         ),
  126.         'dict' => array(
  127.             'port' => 2628
  128.         ),
  129.         'file' => array(
  130.             'ihost' => 'localhost'
  131.         ),
  132.         'http' => array(
  133.             'port' => 80,
  134.         ),
  135.         'https' => array(
  136.             'port' => 443,
  137.         ),
  138.     );
  139.  
  140.     /**
  141.      * Return the entire IRI when you try and read the object as a string
  142.      *
  143.      * @return string
  144.      */
  145.     public function __toString() {
  146.         return $this->get_iri();
  147.     }
  148.  
  149.     /**
  150.      * Overload __set() to provide access via properties
  151.      *
  152.      * @param string $name Property name
  153.      * @param mixed $value Property value
  154.      */
  155.     public function __set($name, $value) {
  156.         if (method_exists($this, 'set_' . $name)) {
  157.             call_user_func(array($this, 'set_' . $name), $value);
  158.         }
  159.         elseif (
  160.                $name === 'iauthority'
  161.             || $name === 'iuserinfo'
  162.             || $name === 'ihost'
  163.             || $name === 'ipath'
  164.             || $name === 'iquery'
  165.             || $name === 'ifragment'
  166.         ) {
  167.             call_user_func(array($this, 'set_' . substr($name, 1)), $value);
  168.         }
  169.     }
  170.  
  171.     /**
  172.      * Overload __get() to provide access via properties
  173.      *
  174.      * @param string $name Property name
  175.      * @return mixed
  176.      */
  177.     public function __get($name) {
  178.         // isset() returns false for null, we don't want to do that
  179.         // Also why we use array_key_exists below instead of isset()
  180.         $props = get_object_vars($this);
  181.  
  182.         if (
  183.             $name === 'iri' ||
  184.             $name === 'uri' ||
  185.             $name === 'iauthority' ||
  186.             $name === 'authority'
  187.         ) {
  188.             $method = 'get_' . $name;
  189.             $return = $this->$method();
  190.         }
  191.         elseif (array_key_exists($name, $props)) {
  192.             $return = $this->$name;
  193.         }
  194.         // host -> ihost
  195.         elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) {
  196.             $name = $prop;
  197.             $return = $this->$prop;
  198.         }
  199.         // ischeme -> scheme
  200.         elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) {
  201.             $name = $prop;
  202.             $return = $this->$prop;
  203.         }
  204.         else {
  205.             trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE);
  206.             $return = null;
  207.         }
  208.  
  209.         if ($return === null && isset($this->normalization[$this->scheme][$name])) {
  210.             return $this->normalization[$this->scheme][$name];
  211.         }
  212.         else {
  213.             return $return;
  214.         }
  215.     }
  216.  
  217.     /**
  218.      * Overload __isset() to provide access via properties
  219.      *
  220.      * @param string $name Property name
  221.      * @return bool
  222.      */
  223.     public function __isset($name) {
  224.         return (method_exists($this, 'get_' . $name) || isset($this->$name));
  225.     }
  226.  
  227.     /**
  228.      * Overload __unset() to provide access via properties
  229.      *
  230.      * @param string $name Property name
  231.      */
  232.     public function __unset($name) {
  233.         if (method_exists($this, 'set_' . $name)) {
  234.             call_user_func(array($this, 'set_' . $name), '');
  235.         }
  236.     }
  237.  
  238.     /**
  239.      * Create a new IRI object, from a specified string
  240.      *
  241.      * @param string|null $iri
  242.      */
  243.     public function __construct($iri = null) {
  244.         $this->set_iri($iri);
  245.     }
  246.  
  247.     /**
  248.      * Create a new IRI object by resolving a relative IRI
  249.      *
  250.      * Returns false if $base is not absolute, otherwise an IRI.
  251.      *
  252.      * @param IRI|string $base (Absolute) Base IRI
  253.      * @param IRI|string $relative Relative IRI
  254.      * @return IRI|false
  255.      */
  256.     public static function absolutize($base, $relative) {
  257.         if (!($relative instanceof Requests_IRI)) {
  258.             $relative = new Requests_IRI($relative);
  259.         }
  260.         if (!$relative->is_valid()) {
  261.             return false;
  262.         }
  263.         elseif ($relative->scheme !== null) {
  264.             return clone $relative;
  265.         }
  266.  
  267.         if (!($base instanceof Requests_IRI)) {
  268.             $base = new Requests_IRI($base);
  269.         }
  270.         if ($base->scheme === null || !$base->is_valid()) {
  271.             return false;
  272.         }
  273.  
  274.         if ($relative->get_iri() !== '') {
  275.             if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) {
  276.                 $target = clone $relative;
  277.                 $target->scheme = $base->scheme;
  278.             }
  279.             else {
  280.                 $target = new Requests_IRI;
  281.                 $target->scheme = $base->scheme;
  282.                 $target->iuserinfo = $base->iuserinfo;
  283.                 $target->ihost = $base->ihost;
  284.                 $target->port = $base->port;
  285.                 if ($relative->ipath !== '') {
  286.                     if ($relative->ipath[0] === '/') {
  287.                         $target->ipath = $relative->ipath;
  288.                     }
  289.                     elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') {
  290.                         $target->ipath = '/' . $relative->ipath;
  291.                     }
  292.                     elseif (($last_segment = strrpos($base->ipath, '/')) !== false) {
  293.                         $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath;
  294.                     }
  295.                     else {
  296.                         $target->ipath = $relative->ipath;
  297.                     }
  298.                     $target->ipath = $target->remove_dot_segments($target->ipath);
  299.                     $target->iquery = $relative->iquery;
  300.                 }
  301.                 else {
  302.                     $target->ipath = $base->ipath;
  303.                     if ($relative->iquery !== null) {
  304.                         $target->iquery = $relative->iquery;
  305.                     }
  306.                     elseif ($base->iquery !== null) {
  307.                         $target->iquery = $base->iquery;
  308.                     }
  309.                 }
  310.                 $target->ifragment = $relative->ifragment;
  311.             }
  312.         }
  313.         else {
  314.             $target = clone $base;
  315.             $target->ifragment = null;
  316.         }
  317.         $target->scheme_normalization();
  318.         return $target;
  319.     }
  320.  
  321.     /**
  322.      * Parse an IRI into scheme/authority/path/query/fragment segments
  323.      *
  324.      * @param string $iri
  325.      * @return array
  326.      */
  327.     protected function parse_iri($iri) {
  328.         $iri = trim($iri, "\x20\x09\x0A\x0C\x0D");
  329.         $has_match = preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match);
  330.         if (!$has_match) {
  331.             throw new Requests_Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri);
  332.         }
  333.  
  334.         if ($match[1] === '') {
  335.             $match['scheme'] = null;
  336.         }
  337.         if (!isset($match[3]) || $match[3] === '') {
  338.             $match['authority'] = null;
  339.         }
  340.         if (!isset($match[5])) {
  341.             $match['path'] = '';
  342.         }
  343.         if (!isset($match[6]) || $match[6] === '') {
  344.             $match['query'] = null;
  345.         }
  346.         if (!isset($match[8]) || $match[8] === '') {
  347.             $match['fragment'] = null;
  348.         }
  349.         return $match;
  350.     }
  351.  
  352.     /**
  353.      * Remove dot segments from a path
  354.      *
  355.      * @param string $input
  356.      * @return string
  357.      */
  358.     protected function remove_dot_segments($input) {
  359.         $output = '';
  360.         while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') {
  361.             // A: If the input buffer begins with a prefix of "../" or "./",
  362.             // then remove that prefix from the input buffer; otherwise,
  363.             if (strpos($input, '../') === 0) {
  364.                 $input = substr($input, 3);
  365.             }
  366.             elseif (strpos($input, './') === 0) {
  367.                 $input = substr($input, 2);
  368.             }
  369.             // B: if the input buffer begins with a prefix of "/./" or "/.",
  370.             // where "." is a complete path segment, then replace that prefix
  371.             // with "/" in the input buffer; otherwise,
  372.             elseif (strpos($input, '/./') === 0) {
  373.                 $input = substr($input, 2);
  374.             }
  375.             elseif ($input === '/.') {
  376.                 $input = '/';
  377.             }
  378.             // C: if the input buffer begins with a prefix of "/../" or "/..",
  379.             // where ".." is a complete path segment, then replace that prefix
  380.             // with "/" in the input buffer and remove the last segment and its
  381.             // preceding "/" (if any) from the output buffer; otherwise,
  382.             elseif (strpos($input, '/../') === 0) {
  383.                 $input = substr($input, 3);
  384.                 $output = substr_replace($output, '', strrpos($output, '/'));
  385.             }
  386.             elseif ($input === '/..') {
  387.                 $input = '/';
  388.                 $output = substr_replace($output, '', strrpos($output, '/'));
  389.             }
  390.             // D: if the input buffer consists only of "." or "..", then remove
  391.             // that from the input buffer; otherwise,
  392.             elseif ($input === '.' || $input === '..') {
  393.                 $input = '';
  394.             }
  395.             // E: move the first path segment in the input buffer to the end of
  396.             // the output buffer, including the initial "/" character (if any)
  397.             // and any subsequent characters up to, but not including, the next
  398.             // "/" character or the end of the input buffer
  399.             elseif (($pos = strpos($input, '/', 1)) !== false) {
  400.                 $output .= substr($input, 0, $pos);
  401.                 $input = substr_replace($input, '', 0, $pos);
  402.             }
  403.             else {
  404.                 $output .= $input;
  405.                 $input = '';
  406.             }
  407.         }
  408.         return $output . $input;
  409.     }
  410.  
  411.     /**
  412.      * Replace invalid character with percent encoding
  413.      *
  414.      * @param string $string Input string
  415.      * @param string $extra_chars Valid characters not in iunreserved or
  416.      *                            iprivate (this is ASCII-only)
  417.      * @param bool $iprivate Allow iprivate
  418.      * @return string
  419.      */
  420.     protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) {
  421.         // Normalize as many pct-encoded sections as possible
  422.         $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array(&$this, 'remove_iunreserved_percent_encoded'), $string);
  423.  
  424.         // Replace invalid percent characters
  425.         $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string);
  426.  
  427.         // Add unreserved and % to $extra_chars (the latter is safe because all
  428.         // pct-encoded sections are now valid).
  429.         $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%';
  430.  
  431.         // Now replace any bytes that aren't allowed with their pct-encoded versions
  432.         $position = 0;
  433.         $strlen = strlen($string);
  434.         while (($position += strspn($string, $extra_chars, $position)) < $strlen) {
  435.             $value = ord($string[$position]);
  436.  
  437.             // Start position
  438.             $start = $position;
  439.  
  440.             // By default we are valid
  441.             $valid = true;
  442.  
  443.             // No one byte sequences are valid due to the while.
  444.             // Two byte sequence:
  445.             if (($value & 0xE0) === 0xC0) {
  446.                 $character = ($value & 0x1F) << 6;
  447.                 $length = 2;
  448.                 $remaining = 1;
  449.             }
  450.             // Three byte sequence:
  451.             elseif (($value & 0xF0) === 0xE0) {
  452.                 $character = ($value & 0x0F) << 12;
  453.                 $length = 3;
  454.                 $remaining = 2;
  455.             }
  456.             // Four byte sequence:
  457.             elseif (($value & 0xF8) === 0xF0) {
  458.                 $character = ($value & 0x07) << 18;
  459.                 $length = 4;
  460.                 $remaining = 3;
  461.             }
  462.             // Invalid byte:
  463.             else {
  464.                 $valid = false;
  465.                 $length = 1;
  466.                 $remaining = 0;
  467.             }
  468.  
  469.             if ($remaining) {
  470.                 if ($position + $length <= $strlen) {
  471.                     for ($position++; $remaining; $position++) {
  472.                         $value = ord($string[$position]);
  473.  
  474.                         // Check that the byte is valid, then add it to the character:
  475.                         if (($value & 0xC0) === 0x80) {
  476.                             $character |= ($value & 0x3F) << (--$remaining * 6);
  477.                         }
  478.                         // If it is invalid, count the sequence as invalid and reprocess the current byte:
  479.                         else {
  480.                             $valid = false;
  481.                             $position--;
  482.                             break;
  483.                         }
  484.                     }
  485.                 }
  486.                 else {
  487.                     $position = $strlen - 1;
  488.                     $valid = false;
  489.                 }
  490.             }
  491.  
  492.             // Percent encode anything invalid or not in ucschar
  493.             if (
  494.                 // Invalid sequences
  495.                 !$valid
  496.                 // Non-shortest form sequences are invalid
  497.                 || $length > 1 && $character <= 0x7F
  498.                 || $length > 2 && $character <= 0x7FF
  499.                 || $length > 3 && $character <= 0xFFFF
  500.                 // Outside of range of ucschar codepoints
  501.                 // Noncharacters
  502.                 || ($character & 0xFFFE) === 0xFFFE
  503.                 || $character >= 0xFDD0 && $character <= 0xFDEF
  504.                 || (
  505.                     // Everything else not in ucschar
  506.                        $character > 0xD7FF && $character < 0xF900
  507.                     || $character < 0xA0
  508.                     || $character > 0xEFFFD
  509.                 )
  510.                 && (
  511.                     // Everything not in iprivate, if it applies
  512.                        !$iprivate
  513.                     || $character < 0xE000
  514.                     || $character > 0x10FFFD
  515.                 )
  516.             ) {
  517.                 // If we were a character, pretend we weren't, but rather an error.
  518.                 if ($valid) {
  519.                     $position--;
  520.                 }
  521.  
  522.                 for ($j = $start; $j <= $position; $j++) {
  523.                     $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1);
  524.                     $j += 2;
  525.                     $position += 2;
  526.                     $strlen += 2;
  527.                 }
  528.             }
  529.         }
  530.  
  531.         return $string;
  532.     }
  533.  
  534.     /**
  535.      * Callback function for preg_replace_callback.
  536.      *
  537.      * Removes sequences of percent encoded bytes that represent UTF-8
  538.      * encoded characters in iunreserved
  539.      *
  540.      * @param array $match PCRE match
  541.      * @return string Replacement
  542.      */
  543.     protected function remove_iunreserved_percent_encoded($match) {
  544.         // As we just have valid percent encoded sequences we can just explode
  545.         // and ignore the first member of the returned array (an empty string).
  546.         $bytes = explode('%', $match[0]);
  547.  
  548.         // Initialize the new string (this is what will be returned) and that
  549.         // there are no bytes remaining in the current sequence (unsurprising
  550.         // at the first byte!).
  551.         $string = '';
  552.         $remaining = 0;
  553.  
  554.         // Loop over each and every byte, and set $value to its value
  555.         for ($i = 1, $len = count($bytes); $i < $len; $i++) {
  556.             $value = hexdec($bytes[$i]);
  557.  
  558.             // If we're the first byte of sequence:
  559.             if (!$remaining) {
  560.                 // Start position
  561.                 $start = $i;
  562.  
  563.                 // By default we are valid
  564.                 $valid = true;
  565.  
  566.                 // One byte sequence:
  567.                 if ($value <= 0x7F) {
  568.                     $character = $value;
  569.                     $length = 1;
  570.                 }
  571.                 // Two byte sequence:
  572.                 elseif (($value & 0xE0) === 0xC0) {
  573.                     $character = ($value & 0x1F) << 6;
  574.                     $length = 2;
  575.                     $remaining = 1;
  576.                 }
  577.                 // Three byte sequence:
  578.                 elseif (($value & 0xF0) === 0xE0) {
  579.                     $character = ($value & 0x0F) << 12;
  580.                     $length = 3;
  581.                     $remaining = 2;
  582.                 }
  583.                 // Four byte sequence:
  584.                 elseif (($value & 0xF8) === 0xF0) {
  585.                     $character = ($value & 0x07) << 18;
  586.                     $length = 4;
  587.                     $remaining = 3;
  588.                 }
  589.                 // Invalid byte:
  590.                 else {
  591.                     $valid = false;
  592.                     $remaining = 0;
  593.                 }
  594.             }
  595.             // Continuation byte:
  596.             else {
  597.                 // Check that the byte is valid, then add it to the character:
  598.                 if (($value & 0xC0) === 0x80) {
  599.                     $remaining--;
  600.                     $character |= ($value & 0x3F) << ($remaining * 6);
  601.                 }
  602.                 // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence:
  603.                 else {
  604.                     $valid = false;
  605.                     $remaining = 0;
  606.                     $i--;
  607.                 }
  608.             }
  609.  
  610.             // If we've reached the end of the current byte sequence, append it to Unicode::$data
  611.             if (!$remaining) {
  612.                 // Percent encode anything invalid or not in iunreserved
  613.                 if (
  614.                     // Invalid sequences
  615.                     !$valid
  616.                     // Non-shortest form sequences are invalid
  617.                     || $length > 1 && $character <= 0x7F
  618.                     || $length > 2 && $character <= 0x7FF
  619.                     || $length > 3 && $character <= 0xFFFF
  620.                     // Outside of range of iunreserved codepoints
  621.                     || $character < 0x2D
  622.                     || $character > 0xEFFFD
  623.                     // Noncharacters
  624.                     || ($character & 0xFFFE) === 0xFFFE
  625.                     || $character >= 0xFDD0 && $character <= 0xFDEF
  626.                     // Everything else not in iunreserved (this is all BMP)
  627.                     || $character === 0x2F
  628.                     || $character > 0x39 && $character < 0x41
  629.                     || $character > 0x5A && $character < 0x61
  630.                     || $character > 0x7A && $character < 0x7E
  631.                     || $character > 0x7E && $character < 0xA0
  632.                     || $character > 0xD7FF && $character < 0xF900
  633.                 ) {
  634.                     for ($j = $start; $j <= $i; $j++) {
  635.                         $string .= '%' . strtoupper($bytes[$j]);
  636.                     }
  637.                 }
  638.                 else {
  639.                     for ($j = $start; $j <= $i; $j++) {
  640.                         $string .= chr(hexdec($bytes[$j]));
  641.                     }
  642.                 }
  643.             }
  644.         }
  645.  
  646.         // If we have any bytes left over they are invalid (i.e., we are
  647.         // mid-way through a multi-byte sequence)
  648.         if ($remaining) {
  649.             for ($j = $start; $j < $len; $j++) {
  650.                 $string .= '%' . strtoupper($bytes[$j]);
  651.             }
  652.         }
  653.  
  654.         return $string;
  655.     }
  656.  
  657.     protected function scheme_normalization() {
  658.         if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) {
  659.             $this->iuserinfo = null;
  660.         }
  661.         if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) {
  662.             $this->ihost = null;
  663.         }
  664.         if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) {
  665.             $this->port = null;
  666.         }
  667.         if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) {
  668.             $this->ipath = '';
  669.         }
  670.         if (isset($this->ihost) && empty($this->ipath)) {
  671.             $this->ipath = '/';
  672.         }
  673.         if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) {
  674.             $this->iquery = null;
  675.         }
  676.         if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) {
  677.             $this->ifragment = null;
  678.         }
  679.     }
  680.  
  681.     /**
  682.      * Check if the object represents a valid IRI. This needs to be done on each
  683.      * call as some things change depending on another part of the IRI.
  684.      *
  685.      * @return bool
  686.      */
  687.     public function is_valid() {
  688.         $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null;
  689.         if ($this->ipath !== '' &&
  690.             (
  691.                 $isauthority && $this->ipath[0] !== '/' ||
  692.                 (
  693.                     $this->scheme === null &&
  694.                     !$isauthority &&
  695.                     strpos($this->ipath, ':') !== false &&
  696.                     (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/'))
  697.                 )
  698.             )
  699.         ) {
  700.             return false;
  701.         }
  702.  
  703.         return true;
  704.     }
  705.  
  706.     /**
  707.      * Set the entire IRI. Returns true on success, false on failure (if there
  708.      * are any invalid characters).
  709.      *
  710.      * @param string $iri
  711.      * @return bool
  712.      */
  713.     protected function set_iri($iri) {
  714.         static $cache;
  715.         if (!$cache) {
  716.             $cache = array();
  717.         }
  718.  
  719.         if ($iri === null) {
  720.             return true;
  721.         }
  722.         if (isset($cache[$iri])) {
  723.             list($this->scheme,
  724.                  $this->iuserinfo,
  725.                  $this->ihost,
  726.                  $this->port,
  727.                  $this->ipath,
  728.                  $this->iquery,
  729.                  $this->ifragment,
  730.                  $return) = $cache[$iri];
  731.             return $return;
  732.         }
  733.  
  734.         $parsed = $this->parse_iri((string) $iri);
  735.  
  736.         $return = $this->set_scheme($parsed['scheme'])
  737.             && $this->set_authority($parsed['authority'])
  738.             && $this->set_path($parsed['path'])
  739.             && $this->set_query($parsed['query'])
  740.             && $this->set_fragment($parsed['fragment']);
  741.  
  742.         $cache[$iri] = array($this->scheme,
  743.                              $this->iuserinfo,
  744.                              $this->ihost,
  745.                              $this->port,
  746.                              $this->ipath,
  747.                              $this->iquery,
  748.                              $this->ifragment,
  749.                              $return);
  750.         return $return;
  751.     }
  752.  
  753.     /**
  754.      * Set the scheme. Returns true on success, false on failure (if there are
  755.      * any invalid characters).
  756.      *
  757.      * @param string $scheme
  758.      * @return bool
  759.      */
  760.     protected function set_scheme($scheme) {
  761.         if ($scheme === null) {
  762.             $this->scheme = null;
  763.         }
  764.         elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) {
  765.             $this->scheme = null;
  766.             return false;
  767.         }
  768.         else {
  769.             $this->scheme = strtolower($scheme);
  770.         }
  771.         return true;
  772.     }
  773.  
  774.     /**
  775.      * Set the authority. Returns true on success, false on failure (if there are
  776.      * any invalid characters).
  777.      *
  778.      * @param string $authority
  779.      * @return bool
  780.      */
  781.     protected function set_authority($authority) {
  782.         static $cache;
  783.         if (!$cache) {
  784.             $cache = array();
  785.         }
  786.  
  787.         if ($authority === null) {
  788.             $this->iuserinfo = null;
  789.             $this->ihost = null;
  790.             $this->port = null;
  791.             return true;
  792.         }
  793.         if (isset($cache[$authority])) {
  794.             list($this->iuserinfo,
  795.                  $this->ihost,
  796.                  $this->port,
  797.                  $return) = $cache[$authority];
  798.  
  799.             return $return;
  800.         }
  801.  
  802.         $remaining = $authority;
  803.         if (($iuserinfo_end = strrpos($remaining, '@')) !== false) {
  804.             $iuserinfo = substr($remaining, 0, $iuserinfo_end);
  805.             $remaining = substr($remaining, $iuserinfo_end + 1);
  806.         }
  807.         else {
  808.             $iuserinfo = null;
  809.         }
  810.         if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) {
  811.             $port = substr($remaining, $port_start + 1);
  812.             if ($port === false || $port === '') {
  813.                 $port = null;
  814.             }
  815.             $remaining = substr($remaining, 0, $port_start);
  816.         }
  817.         else {
  818.             $port = null;
  819.         }
  820.  
  821.         $return = $this->set_userinfo($iuserinfo) &&
  822.                   $this->set_host($remaining) &&
  823.                   $this->set_port($port);
  824.  
  825.         $cache[$authority] = array($this->iuserinfo,
  826.                                    $this->ihost,
  827.                                    $this->port,
  828.                                    $return);
  829.  
  830.         return $return;
  831.     }
  832.  
  833.     /**
  834.      * Set the iuserinfo.
  835.      *
  836.      * @param string $iuserinfo
  837.      * @return bool
  838.      */
  839.     protected function set_userinfo($iuserinfo) {
  840.         if ($iuserinfo === null) {
  841.             $this->iuserinfo = null;
  842.         }
  843.         else {
  844.             $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
  845.             $this->scheme_normalization();
  846.         }
  847.  
  848.         return true;
  849.     }
  850.  
  851.     /**
  852.      * Set the ihost. Returns true on success, false on failure (if there are
  853.      * any invalid characters).
  854.      *
  855.      * @param string $ihost
  856.      * @return bool
  857.      */
  858.     protected function set_host($ihost) {
  859.         if ($ihost === null) {
  860.             $this->ihost = null;
  861.             return true;
  862.         }
  863.         if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') {
  864.             if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) {
  865.                 $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']';
  866.             }
  867.             else {
  868.                 $this->ihost = null;
  869.                 return false;
  870.             }
  871.         }
  872.         else {
  873.             $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');
  874.  
  875.             // Lowercase, but ignore pct-encoded sections (as they should
  876.             // remain uppercase). This must be done after the previous step
  877.             // as that can add unescaped characters.
  878.             $position = 0;
  879.             $strlen = strlen($ihost);
  880.             while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) {
  881.                 if ($ihost[$position] === '%') {
  882.                     $position += 3;
  883.                 }
  884.                 else {
  885.                     $ihost[$position] = strtolower($ihost[$position]);
  886.                     $position++;
  887.                 }
  888.             }
  889.  
  890.             $this->ihost = $ihost;
  891.         }
  892.  
  893.         $this->scheme_normalization();
  894.  
  895.         return true;
  896.     }
  897.  
  898.     /**
  899.      * Set the port. Returns true on success, false on failure (if there are
  900.      * any invalid characters).
  901.      *
  902.      * @param string $port
  903.      * @return bool
  904.      */
  905.     protected function set_port($port) {
  906.         if ($port === null) {
  907.             $this->port = null;
  908.             return true;
  909.         }
  910.  
  911.         if (strspn($port, '0123456789') === strlen($port)) {
  912.             $this->port = (int) $port;
  913.             $this->scheme_normalization();
  914.             return true;
  915.         }
  916.  
  917.         $this->port = null;
  918.         return false;
  919.     }
  920.  
  921.     /**
  922.      * Set the ipath.
  923.      *
  924.      * @param string $ipath
  925.      * @return bool
  926.      */
  927.     protected function set_path($ipath) {
  928.         static $cache;
  929.         if (!$cache) {
  930.             $cache = array();
  931.         }
  932.  
  933.         $ipath = (string) $ipath;
  934.  
  935.         if (isset($cache[$ipath])) {
  936.             $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
  937.         }
  938.         else {
  939.             $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
  940.             $removed = $this->remove_dot_segments($valid);
  941.  
  942.             $cache[$ipath] = array($valid, $removed);
  943.             $this->ipath = ($this->scheme !== null) ? $removed : $valid;
  944.         }
  945.         $this->scheme_normalization();
  946.         return true;
  947.     }
  948.  
  949.     /**
  950.      * Set the iquery.
  951.      *
  952.      * @param string $iquery
  953.      * @return bool
  954.      */
  955.     protected function set_query($iquery) {
  956.         if ($iquery === null) {
  957.             $this->iquery = null;
  958.         }
  959.         else {
  960.             $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
  961.             $this->scheme_normalization();
  962.         }
  963.         return true;
  964.     }
  965.  
  966.     /**
  967.      * Set the ifragment.
  968.      *
  969.      * @param string $ifragment
  970.      * @return bool
  971.      */
  972.     protected function set_fragment($ifragment) {
  973.         if ($ifragment === null) {
  974.             $this->ifragment = null;
  975.         }
  976.         else {
  977.             $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
  978.             $this->scheme_normalization();
  979.         }
  980.         return true;
  981.     }
  982.  
  983.     /**
  984.      * Convert an IRI to a URI (or parts thereof)
  985.      *
  986.      * @param string|bool IRI to convert (or false from {@see get_iri})
  987.      * @return string|false URI if IRI is valid, false otherwise.
  988.      */
  989.     protected function to_uri($string) {
  990.         if (!is_string($string)) {
  991.             return false;
  992.         }
  993.  
  994.         static $non_ascii;
  995.         if (!$non_ascii) {
  996.             $non_ascii = implode('', range("\x80", "\xFF"));
  997.         }
  998.  
  999.         $position = 0;
  1000.         $strlen = strlen($string);
  1001.         while (($position += strcspn($string, $non_ascii, $position)) < $strlen) {
  1002.             $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1);
  1003.             $position += 3;
  1004.             $strlen += 2;
  1005.         }
  1006.  
  1007.         return $string;
  1008.     }
  1009.  
  1010.     /**
  1011.      * Get the complete IRI
  1012.      *
  1013.      * @return string
  1014.      */
  1015.     protected function get_iri() {
  1016.         if (!$this->is_valid()) {
  1017.             return false;
  1018.         }
  1019.  
  1020.         $iri = '';
  1021.         if ($this->scheme !== null) {
  1022.             $iri .= $this->scheme . ':';
  1023.         }
  1024.         if (($iauthority = $this->get_iauthority()) !== null) {
  1025.             $iri .= '//' . $iauthority;
  1026.         }
  1027.         $iri .= $this->ipath;
  1028.         if ($this->iquery !== null) {
  1029.             $iri .= '?' . $this->iquery;
  1030.         }
  1031.         if ($this->ifragment !== null) {
  1032.             $iri .= '#' . $this->ifragment;
  1033.         }
  1034.  
  1035.         return $iri;
  1036.     }
  1037.  
  1038.     /**
  1039.      * Get the complete URI
  1040.      *
  1041.      * @return string
  1042.      */
  1043.     protected function get_uri() {
  1044.         return $this->to_uri($this->get_iri());
  1045.     }
  1046.  
  1047.     /**
  1048.      * Get the complete iauthority
  1049.      *
  1050.      * @return string
  1051.      */
  1052.     protected function get_iauthority() {
  1053.         if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) {
  1054.             return null;
  1055.         }
  1056.  
  1057.         $iauthority = '';
  1058.         if ($this->iuserinfo !== null) {
  1059.             $iauthority .= $this->iuserinfo . '@';
  1060.         }
  1061.         if ($this->ihost !== null) {
  1062.             $iauthority .= $this->ihost;
  1063.         }
  1064.         if ($this->port !== null) {
  1065.             $iauthority .= ':' . $this->port;
  1066.         }
  1067.         return $iauthority;
  1068.     }
  1069.  
  1070.     /**
  1071.      * Get the complete authority
  1072.      *
  1073.      * @return string
  1074.      */
  1075.     protected function get_authority() {
  1076.         $iauthority = $this->get_iauthority();
  1077.         if (is_string($iauthority)) {
  1078.             return $this->to_uri($iauthority);
  1079.         }
  1080.         else {
  1081.             return $iauthority;
  1082.         }
  1083.     }
  1084. }
  1085.