TYPO3  7.6
URL2.php
Go to the documentation of this file.
1 <?php
57 class Net_URL2
58 {
63  const OPTION_STRICT = 'strict';
64 
68  const OPTION_USE_BRACKETS = 'use_brackets';
69 
74  const OPTION_DROP_SEQUENCE = 'drop_sequence';
75 
79  const OPTION_ENCODE_KEYS = 'encode_keys';
80 
85  const OPTION_SEPARATOR_INPUT = 'input_separator';
86 
91  const OPTION_SEPARATOR_OUTPUT = 'output_separator';
92 
96  private $_options = array(
97  self::OPTION_STRICT => true,
98  self::OPTION_USE_BRACKETS => true,
99  self::OPTION_DROP_SEQUENCE => true,
100  self::OPTION_ENCODE_KEYS => true,
101  self::OPTION_SEPARATOR_INPUT => '&',
102  self::OPTION_SEPARATOR_OUTPUT => '&',
103  );
104 
108  private $_scheme = false;
109 
113  private $_userinfo = false;
114 
118  private $_host = false;
119 
123  private $_port = false;
124 
128  private $_path = '';
129 
133  private $_query = false;
134 
138  private $_fragment = false;
139 
148  public function __construct($url, array $options = array())
149  {
150  foreach ($options as $optionName => $value) {
151  if (array_key_exists($optionName, $this->_options)) {
152  $this->_options[$optionName] = $value;
153  }
154  }
155 
156  $this->parseUrl($url);
157  }
158 
170  public function __set($var, $arg)
171  {
172  $method = 'set' . $var;
173  if (method_exists($this, $method)) {
174  $this->$method($arg);
175  }
176  }
177 
190  public function __get($var)
191  {
192  $method = 'get' . $var;
193  if (method_exists($this, $method)) {
194  return $this->$method();
195  }
196 
197  return false;
198  }
199 
206  public function getScheme()
207  {
208  return $this->_scheme;
209  }
210 
222  public function setScheme($scheme)
223  {
224  $this->_scheme = $scheme;
225  return $this;
226  }
227 
234  public function getUser()
235  {
236  return $this->_userinfo !== false
237  ? preg_replace('(:.*$)', '', $this->_userinfo)
238  : false;
239  }
240 
249  public function getPassword()
250  {
251  return $this->_userinfo !== false
252  ? substr(strstr($this->_userinfo, ':'), 1)
253  : false;
254  }
255 
262  public function getUserinfo()
263  {
264  return $this->_userinfo;
265  }
266 
276  public function setUserinfo($userinfo, $password = false)
277  {
278  if ($password !== false) {
279  $userinfo .= ':' . $password;
280  }
281 
282  if ($userinfo !== false) {
283  $userinfo = $this->_encodeData($userinfo);
284  }
285 
286  $this->_userinfo = $userinfo;
287  return $this;
288  }
289 
296  public function getHost()
297  {
298  return $this->_host;
299  }
300 
309  public function setHost($host)
310  {
311  $this->_host = $host;
312  return $this;
313  }
314 
321  public function getPort()
322  {
323  return $this->_port;
324  }
325 
334  public function setPort($port)
335  {
336  $this->_port = $port;
337  return $this;
338  }
339 
346  public function getAuthority()
347  {
348  if (false === $this->_host) {
349  return false;
350  }
351 
352  $authority = '';
353 
354  if (strlen($this->_userinfo)) {
355  $authority .= $this->_userinfo . '@';
356  }
357 
358  $authority .= $this->_host;
359 
360  if ($this->_port !== false) {
361  $authority .= ':' . $this->_port;
362  }
363 
364  return $authority;
365  }
366 
377  public function setAuthority($authority)
378  {
379  $this->_userinfo = false;
380  $this->_host = false;
381  $this->_port = false;
382 
383  if ('' === $authority) {
384  $this->_host = $authority;
385  return $this;
386  }
387 
388  if (!preg_match('(^(([^\@]*)\@)?(.+?)(:(\d*))?$)', $authority, $matches)) {
389  return $this;
390  }
391 
392  if ($matches[1]) {
393  $this->_userinfo = $this->_encodeData($matches[2]);
394  }
395 
396  $this->_host = $matches[3];
397 
398  if (isset($matches[5]) && strlen($matches[5])) {
399  $this->_port = $matches[5];
400  }
401  return $this;
402  }
403 
409  public function getPath()
410  {
411  return $this->_path;
412  }
413 
421  public function setPath($path)
422  {
423  $this->_path = $path;
424  return $this;
425  }
426 
434  public function getQuery()
435  {
436  return $this->_query;
437  }
438 
448  public function setQuery($query)
449  {
450  $this->_query = $query;
451  return $this;
452  }
453 
459  public function getFragment()
460  {
461  return $this->_fragment;
462  }
463 
472  public function setFragment($fragment)
473  {
474  $this->_fragment = $fragment;
475  return $this;
476  }
477 
485  public function getQueryVariables()
486  {
487  $separator = $this->getOption(self::OPTION_SEPARATOR_INPUT);
488  $encodeKeys = $this->getOption(self::OPTION_ENCODE_KEYS);
489  $useBrackets = $this->getOption(self::OPTION_USE_BRACKETS);
490 
491  $return = array();
492 
493  for ($part = strtok($this->_query, $separator);
494  strlen($part);
495  $part = strtok($separator)
496  ) {
497  list($key, $value) = explode('=', $part, 2) + array(1 => '');
498 
499  if ($encodeKeys) {
500  $key = rawurldecode($key);
501  }
502  $value = rawurldecode($value);
503 
504  if ($useBrackets) {
505  $return = $this->_queryArrayByKey($key, $value, $return);
506  } else {
507  if (isset($return[$key])) {
508  $return[$key] = (array) $return[$key];
509  $return[$key][] = $value;
510  } else {
511  $return[$key] = $value;
512  }
513  }
514  }
515 
516  return $return;
517  }
518 
528  private function _queryArrayByKey($key, $value, array $array = array())
529  {
530  if (!strlen($key)) {
531  return $array;
532  }
533 
534  $offset = $this->_queryKeyBracketOffset($key);
535  if ($offset === false) {
536  $name = $key;
537  } else {
538  $name = substr($key, 0, $offset);
539  }
540 
541  if (!strlen($name)) {
542  return $array;
543  }
544 
545  if (!$offset) {
546  // named value
547  $array[$name] = $value;
548  } else {
549  // array
550  $brackets = substr($key, $offset);
551  if (!isset($array[$name])) {
552  $array[$name] = null;
553  }
554  $array[$name] = $this->_queryArrayByBrackets(
555  $brackets, $value, $array[$name]
556  );
557  }
558 
559  return $array;
560  }
561 
572  private function _queryArrayByBrackets($buffer, $value, array $array = null)
573  {
574  $entry = &$array;
575 
576  for ($iteration = 0; strlen($buffer); $iteration++) {
577  $open = $this->_queryKeyBracketOffset($buffer);
578  if ($open !== 0) {
579  // Opening bracket [ must exist at offset 0, if not, there is
580  // no bracket to parse and the value dropped.
581  // if this happens in the first iteration, this is flawed, see
582  // as well the second exception below.
583  if ($iteration) {
584  break;
585  }
586  // @codeCoverageIgnoreStart
587  throw new Exception(
588  'Net_URL2 Internal Error: '. __METHOD__ .'(): ' .
589  'Opening bracket [ must exist at offset 0'
590  );
591  // @codeCoverageIgnoreEnd
592  }
593 
594  $close = strpos($buffer, ']', 1);
595  if (!$close) {
596  // this error condition should never be reached as this is a
597  // private method and bracket pairs are checked beforehand.
598  // See as well the first exception for the opening bracket.
599  // @codeCoverageIgnoreStart
600  throw new Exception(
601  'Net_URL2 Internal Error: '. __METHOD__ .'(): ' .
602  'Closing bracket ] must exist, not found'
603  );
604  // @codeCoverageIgnoreEnd
605  }
606 
607  $index = substr($buffer, 1, $close - 1);
608  if (strlen($index)) {
609  $entry = &$entry[$index];
610  } else {
611  if (!is_array($entry)) {
612  $entry = array();
613  }
614  $entry[] = &$new;
615  $entry = &$new;
616  unset($new);
617  }
618  $buffer = substr($buffer, $close + 1);
619  }
620 
621  $entry = $value;
622 
623  return $array;
624  }
625 
633  private function _queryKeyBracketOffset($key)
634  {
635  if (false !== $open = strpos($key, '[')
636  and false === strpos($key, ']', $open + 1)
637  ) {
638  $open = false;
639  }
640 
641  return $open;
642  }
643 
651  public function setQueryVariables(array $array)
652  {
653  if (!$array) {
654  $this->_query = false;
655  } else {
656  $this->_query = $this->buildQuery(
657  $array,
658  $this->getOption(self::OPTION_SEPARATOR_OUTPUT)
659  );
660  }
661  return $this;
662  }
663 
672  public function setQueryVariable($name, $value)
673  {
674  $array = $this->getQueryVariables();
675  $array[$name] = $value;
676  $this->setQueryVariables($array);
677  return $this;
678  }
679 
687  public function unsetQueryVariable($name)
688  {
689  $array = $this->getQueryVariables();
690  unset($array[$name]);
691  $this->setQueryVariables($array);
692  }
693 
699  public function getURL()
700  {
701  // See RFC 3986, section 5.3
702  $url = '';
703 
704  if ($this->_scheme !== false) {
705  $url .= $this->_scheme . ':';
706  }
707 
708  $authority = $this->getAuthority();
709  if ($authority === false && strtolower($this->_scheme) === 'file') {
710  $authority = '';
711  }
712 
713  $url .= $this->_buildAuthorityAndPath($authority, $this->_path);
714 
715  if ($this->_query !== false) {
716  $url .= '?' . $this->_query;
717  }
718 
719  if ($this->_fragment !== false) {
720  $url .= '#' . $this->_fragment;
721  }
722 
723  return $url;
724  }
725 
735  private function _buildAuthorityAndPath($authority, $path)
736  {
737  if ($authority === false) {
738  return $path;
739  }
740 
741  $terminator = ($path !== '' && $path[0] !== '/') ? '/' : '';
742 
743  return '//' . $authority . $terminator . $path;
744  }
745 
752  public function __toString()
753  {
754  return $this->getURL();
755  }
756 
763  public function getNormalizedURL()
764  {
765  $url = clone $this;
766  $url->normalize();
767  return $url->getUrl();
768  }
769 
779  public function normalize()
780  {
781  // See RFC 3986, section 6
782 
783  // Scheme is case-insensitive
784  if ($this->_scheme) {
785  $this->_scheme = strtolower($this->_scheme);
786  }
787 
788  // Hostname is case-insensitive
789  if ($this->_host) {
790  $this->_host = strtolower($this->_host);
791  }
792 
793  // Remove default port number for known schemes (RFC 3986, section 6.2.3)
794  if ('' === $this->_port
795  || $this->_port
796  && $this->_scheme
797  && $this->_port == getservbyname($this->_scheme, 'tcp')
798  ) {
799  $this->_port = false;
800  }
801 
802  // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
803  // Normalize percentage-encoded unreserved characters (section 6.2.2.2)
804  $fields = array(&$this->_userinfo, &$this->_host, &$this->_path,
805  &$this->_query, &$this->_fragment);
806  foreach ($fields as &$field) {
807  if ($field !== false) {
808  $field = $this->_normalize("$field");
809  }
810  }
811  unset($field);
812 
813  // Path segment normalization (RFC 3986, section 6.2.2.3)
814  $this->_path = self::removeDotSegments($this->_path);
815 
816  // Scheme based normalization (RFC 3986, section 6.2.3)
817  if (false !== $this->_host && '' === $this->_path) {
818  $this->_path = '/';
819  }
820 
821  // path should start with '/' if there is authority (section 3.3.)
822  if (strlen($this->getAuthority())
823  && strlen($this->_path)
824  && $this->_path[0] !== '/'
825  ) {
826  $this->_path = '/' . $this->_path;
827  }
828  }
829 
840  private function _normalize($mixed)
841  {
842  return preg_replace_callback(
843  '((?:%[0-9a-fA-Z]{2})+)', array($this, '_normalizeCallback'),
844  $mixed
845  );
846  }
847 
858  private function _normalizeCallback($matches)
859  {
860  return self::urlencode(urldecode($matches[0]));
861  }
862 
868  public function isAbsolute()
869  {
870  return (bool) $this->_scheme;
871  }
872 
882  public function resolve($reference)
883  {
884  if (!$reference instanceof Net_URL2) {
885  $reference = new self($reference);
886  }
887  if (!$reference->_isFragmentOnly() && !$this->isAbsolute()) {
888  throw new Exception(
889  'Base-URL must be absolute if reference is not fragment-only'
890  );
891  }
892 
893  // A non-strict parser may ignore a scheme in the reference if it is
894  // identical to the base URI's scheme.
895  if (!$this->getOption(self::OPTION_STRICT)
896  && $reference->_scheme == $this->_scheme
897  ) {
898  $reference->_scheme = false;
899  }
900 
901  $target = new self('');
902  if ($reference->_scheme !== false) {
903  $target->_scheme = $reference->_scheme;
904  $target->setAuthority($reference->getAuthority());
905  $target->_path = self::removeDotSegments($reference->_path);
906  $target->_query = $reference->_query;
907  } else {
908  $authority = $reference->getAuthority();
909  if ($authority !== false) {
910  $target->setAuthority($authority);
911  $target->_path = self::removeDotSegments($reference->_path);
912  $target->_query = $reference->_query;
913  } else {
914  if ($reference->_path == '') {
915  $target->_path = $this->_path;
916  if ($reference->_query !== false) {
917  $target->_query = $reference->_query;
918  } else {
919  $target->_query = $this->_query;
920  }
921  } else {
922  if (substr($reference->_path, 0, 1) == '/') {
923  $target->_path = self::removeDotSegments($reference->_path);
924  } else {
925  // Merge paths (RFC 3986, section 5.2.3)
926  if ($this->_host !== false && $this->_path == '') {
927  $target->_path = '/' . $reference->_path;
928  } else {
929  $i = strrpos($this->_path, '/');
930  if ($i !== false) {
931  $target->_path = substr($this->_path, 0, $i + 1);
932  }
933  $target->_path .= $reference->_path;
934  }
935  $target->_path = self::removeDotSegments($target->_path);
936  }
937  $target->_query = $reference->_query;
938  }
939  $target->setAuthority($this->getAuthority());
940  }
941  $target->_scheme = $this->_scheme;
942  }
943 
944  $target->_fragment = $reference->_fragment;
945 
946  return $target;
947  }
948 
955  private function _isFragmentOnly()
956  {
957  return (
958  $this->_fragment !== false
959  && $this->_query === false
960  && $this->_path === ''
961  && $this->_port === false
962  && $this->_host === false
963  && $this->_userinfo === false
964  && $this->_scheme === false
965  );
966  }
967 
976  public static function removeDotSegments($path)
977  {
978  $path = (string) $path;
979  $output = '';
980 
981  // Make sure not to be trapped in an infinite loop due to a bug in this
982  // method
983  $loopLimit = 256;
984  $j = 0;
985  while ('' !== $path && $j++ < $loopLimit) {
986  if (substr($path, 0, 2) === './') {
987  // Step 2.A
988  $path = substr($path, 2);
989  } elseif (substr($path, 0, 3) === '../') {
990  // Step 2.A
991  $path = substr($path, 3);
992  } elseif (substr($path, 0, 3) === '/./' || $path === '/.') {
993  // Step 2.B
994  $path = '/' . substr($path, 3);
995  } elseif (substr($path, 0, 4) === '/../' || $path === '/..') {
996  // Step 2.C
997  $path = '/' . substr($path, 4);
998  $i = strrpos($output, '/');
999  $output = $i === false ? '' : substr($output, 0, $i);
1000  } elseif ($path === '.' || $path === '..') {
1001  // Step 2.D
1002  $path = '';
1003  } else {
1004  // Step 2.E
1005  $i = strpos($path, '/', $path[0] === '/');
1006  if ($i === false) {
1007  $output .= $path;
1008  $path = '';
1009  break;
1010  }
1011  $output .= substr($path, 0, $i);
1012  $path = substr($path, $i);
1013  }
1014  }
1015 
1016  if ($path !== '') {
1017  $message = sprintf(
1018  'Unable to remove dot segments; hit loop limit %d (left: %s)',
1019  $j, var_export($path, true)
1020  );
1021  trigger_error($message, E_USER_WARNING);
1022  }
1023 
1024  return $output;
1025  }
1026 
1036  public static function urlencode($string)
1037  {
1038  $encoded = rawurlencode($string);
1039 
1040  // This is only necessary in PHP < 5.3.
1041  $encoded = str_replace('%7E', '~', $encoded);
1042  return $encoded;
1043  }
1044 
1052  public static function getCanonical()
1053  {
1054  if (!isset($_SERVER['REQUEST_METHOD'])) {
1055  // ALERT - no current URL
1056  throw new Exception('Script was not called through a webserver');
1057  }
1058 
1059  // Begin with a relative URL
1060  $url = new self($_SERVER['PHP_SELF']);
1061  $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
1062  $url->_host = $_SERVER['SERVER_NAME'];
1063  $port = $_SERVER['SERVER_PORT'];
1064  if ($url->_scheme == 'http' && $port != 80
1065  || $url->_scheme == 'https' && $port != 443
1066  ) {
1067  $url->_port = $port;
1068  }
1069  return $url;
1070  }
1071 
1077  public static function getRequestedURL()
1078  {
1079  return self::getRequested()->getUrl();
1080  }
1081 
1089  public static function getRequested()
1090  {
1091  if (!isset($_SERVER['REQUEST_METHOD'])) {
1092  // ALERT - no current URL
1093  throw new Exception('Script was not called through a webserver');
1094  }
1095 
1096  // Begin with a relative URL
1097  $url = new self($_SERVER['REQUEST_URI']);
1098  $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
1099  // Set host and possibly port
1100  $url->setAuthority($_SERVER['HTTP_HOST']);
1101  return $url;
1102  }
1103 
1111  public function getOption($optionName)
1112  {
1113  return isset($this->_options[$optionName])
1114  ? $this->_options[$optionName] : false;
1115  }
1116 
1128  protected function buildQuery(array $data, $separator, $key = null)
1129  {
1130  $query = array();
1131  $drop_names = (
1132  $this->_options[self::OPTION_DROP_SEQUENCE] === true
1133  && array_keys($data) === array_keys(array_values($data))
1134  );
1135  foreach ($data as $name => $value) {
1136  if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) {
1137  $name = rawurlencode($name);
1138  }
1139  if ($key !== null) {
1140  if ($this->getOption(self::OPTION_USE_BRACKETS) === true) {
1141  $drop_names && $name = '';
1142  $name = $key . '[' . $name . ']';
1143  } else {
1144  $name = $key;
1145  }
1146  }
1147  if (is_array($value)) {
1148  $query[] = $this->buildQuery($value, $separator, $name);
1149  } else {
1150  $query[] = $name . '=' . rawurlencode($value);
1151  }
1152  }
1153  return implode($separator, $query);
1154  }
1155 
1166  protected function parseUrl($url)
1167  {
1168  // The regular expression is copied verbatim from RFC 3986, appendix B.
1169  // The expression does not validate the URL but matches any string.
1170  preg_match(
1171  '(^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)',
1172  $url, $matches
1173  );
1174 
1175  // "path" is always present (possibly as an empty string); the rest
1176  // are optional.
1177  $this->_scheme = !empty($matches[1]) ? $matches[2] : false;
1178  $this->setAuthority(!empty($matches[3]) ? $matches[4] : false);
1179  $this->_path = $this->_encodeData($matches[5]);
1180  $this->_query = !empty($matches[6])
1181  ? $this->_encodeData($matches[7])
1182  : false
1183  ;
1184  $this->_fragment = !empty($matches[8]) ? $matches[9] : false;
1185  }
1186 
1198  private function _encodeData($url)
1199  {
1200  return preg_replace_callback(
1201  '([\x-\x20\x22\x3C\x3E\x7F-\xFF]+)',
1202  array($this, '_encodeCallback'), $url
1203  );
1204  }
1205 
1215  private function _encodeCallback(array $matches)
1216  {
1217  return rawurlencode($matches[0]);
1218  }
1219 }