1<?php 2/** 3 * This file contains the Horde_Url class for manipulating URLs. 4 * 5 * Copyright 2009-2017 Horde LLC (http://www.horde.org/) 6 * 7 * See the enclosed file COPYING for license information (LGPL). If you 8 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 9 * 10 * @author Jan Schneider <jan@horde.org> 11 * @author Michael Slusarz <slusarz@horde.org> 12 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 13 * @category Horde 14 * @package Url 15 */ 16 17/** 18 * The Horde_Url class represents a single URL and provides methods for 19 * manipulating URLs. 20 * 21 * @author Jan Schneider <jan@horde.org> 22 * @author Michael Slusarz <slusarz@horde.org> 23 * @category Horde 24 * @package Url 25 */ 26class Horde_Url 27{ 28 /** 29 * The anchor string (a/k/a fragment). 30 * 31 * @var string 32 */ 33 public $anchor = ''; 34 35 /** 36 * Any PATH_INFO to be added to the URL. 37 * 38 * @var string 39 */ 40 public $pathInfo; 41 42 /** 43 * The query parameters. 44 * 45 * The keys are paramter names, the values parameter values. Array values 46 * will be added to the URL using name[]=value notation. 47 * 48 * @var array 49 */ 50 public $parameters = array(); 51 52 /** 53 * Whether to output the URL in the raw URL format or HTML-encoded. 54 * 55 * @var boolean 56 */ 57 public $raw; 58 59 /** 60 * A callback function to use when converting to a string. 61 * 62 * @var callback 63 */ 64 public $toStringCallback; 65 66 /** 67 * The basic URL, without query parameters. 68 * 69 * @var string 70 */ 71 public $url; 72 73 /** 74 * Cached parameter list for use in toString(). 75 * 76 * @var array 77 */ 78 protected $_cache; 79 80 /** 81 * Constructor. 82 * 83 * @param string|Horde_Url $url The basic URL, with or without query 84 * parameters. 85 * @param boolean $raw Whether to output the URL in the raw URL 86 * format or HTML-encoded. 87 */ 88 public function __construct($url = '', $raw = null) 89 { 90 if ($url instanceof Horde_Url) { 91 foreach (get_object_vars($url) as $k => $v) { 92 $this->$k = $v; 93 } 94 if (!is_null($raw)) { 95 $this->raw = $raw; 96 } 97 return; 98 } 99 100 if (($pos = strrpos($url, '#')) !== false) { 101 $this->anchor = urldecode(substr($url, $pos + 1)); 102 $url = substr($url, 0, $pos); 103 } 104 105 if (($pos = strrpos($url, '?')) !== false) { 106 $query = substr($url, $pos + 1); 107 $url = substr($url, 0, $pos); 108 109 /* Check if the argument separator has been already 110 * htmlentities-ized in the URL. */ 111 if (strpos($query, '&') !== false) { 112 $query = html_entity_decode($query); 113 if (is_null($raw)) { 114 $raw = false; 115 } 116 } elseif (strpos($query, '&') !== false) { 117 if (is_null($raw)) { 118 $raw = true; 119 } 120 } 121 $pairs = explode('&', $query); 122 foreach ($pairs as $pair) { 123 $result = explode('=', urldecode($pair), 2); 124 $this->add($result[0], isset($result[1]) ? $result[1] : null); 125 } 126 } 127 128 $this->url = $url; 129 $this->raw = $raw; 130 } 131 132 /** 133 * Returns a clone of this object. Useful for chaining. 134 * 135 * @return Horde_Url A clone of this object. 136 */ 137 public function copy() 138 { 139 $url = clone $this; 140 return $url; 141 } 142 143 /** 144 * Adds one or more query parameters. 145 * 146 * @param mixed $parameters Either the name value or an array of 147 * name/value pairs. 148 * @param string $value If specified, the value part ($parameters is 149 * then assumed to just be the parameter name). 150 * 151 * @return Horde_Url This (modified) object, to allow chaining. 152 */ 153 public function add($parameters, $value = null) 154 { 155 if (!is_array($parameters)) { 156 $parameters = array($parameters => $value); 157 } 158 159 foreach ($parameters as $parameter => $value) { 160 if (substr($parameter, -2) == '[]') { 161 $parameter = substr($parameter, 0, -2); 162 if (!isset($this->parameters[$parameter])) { 163 $this->parameters[$parameter] = array(); 164 } 165 $this->parameters[$parameter][] = $value; 166 } else { 167 $this->parameters[$parameter] = $value; 168 } 169 } 170 171 unset($this->_cache); 172 173 return $this; 174 } 175 176 /** 177 * Removes one ore more parameters. 178 * 179 * @param mixed $remove Either a single parameter to remove or an array 180 * of parameters to remove. 181 * 182 * @return Horde_Url This (modified) object, to allow chaining. 183 */ 184 public function remove($parameters) 185 { 186 if (!is_array($parameters)) { 187 $parameters = array($parameters); 188 } 189 190 foreach ($parameters as $parameter) { 191 unset($this->parameters[$parameter]); 192 } 193 194 unset($this->_cache); 195 196 return $this; 197 } 198 199 /** 200 * Sets the URL anchor. 201 * 202 * @param string $anchor An anchor to add. 203 * 204 * @return Horde_Url This (modified) object, to allow chaining. 205 */ 206 public function setAnchor($anchor) 207 { 208 $this->anchor = $anchor; 209 return $this; 210 } 211 212 /** 213 * Sets the $raw value. This call can be chained. 214 * 215 * @param boolean $raw Whether to output the URL in the raw URL format or 216 * HTML-encoded. 217 * 218 * @return Horde_Url This object, to allow chaining. 219 */ 220 public function setRaw($raw) 221 { 222 $this->raw = $raw; 223 return $this; 224 } 225 226 /** 227 * Sets the URL scheme. 228 * 229 * @param string $scheme The URL scheme. 230 * @param boolean $replace Force using $scheme, even if it already 231 * exists? 232 * 233 * @return Horde_Url This object, to allow chaining. 234 */ 235 public function setScheme($scheme = 'http', $replace = false) 236 { 237 $pos = stripos($this->url, '://'); 238 if ($pos === false) { 239 $this->url = $scheme . '://' . $this->url; 240 } elseif ($replace) { 241 $this->url = substr_replace($this->url, $scheme . '://', 0, $pos); 242 } 243 return $this; 244 } 245 246 /** 247 * Creates the full URL string. 248 * 249 * @param boolean $raw Whether to output the URL in the raw URL format 250 * or HTML-encoded. 251 * @param boolean $full Output the full URL? 252 * 253 * @return string The string representation of this object. 254 */ 255 public function toString($raw = false, $full = true) 256 { 257 if ($this->toStringCallback) { 258 $callback = $this->toStringCallback; 259 $this->toStringCallback = null; 260 $ret = call_user_func($callback, $this); 261 $this->toStringCallback = $callback; 262 return $ret; 263 } 264 265 $url = $full 266 ? $this->url 267 : parse_url($this->url, PHP_URL_PATH); 268 269 if (strlen($this->pathInfo)) { 270 $url = rtrim($url, '/') . '/'; 271 if ($raw) { 272 $url .= $this->pathInfo; 273 } else { 274 $url .= implode('/', array_map('rawurlencode', explode('/', $this->pathInfo))); 275 } 276 } 277 278 if ($params = $this->_getParameters()) { 279 $url .= '?' . implode($raw ? '&' : '&', $params); 280 } 281 282 if ($this->anchor) { 283 $url .= '#' . ($raw ? $this->anchor : rawurlencode($this->anchor)); 284 } 285 286 return strval($url); 287 } 288 289 /** 290 * Return a formatted list of URL parameters. 291 * 292 * @return array Parameter list. 293 */ 294 protected function _getParameters() 295 { 296 if (!isset($this->_cache)) { 297 $params = array(); 298 299 foreach ($this->parameters as $p => $v) { 300 if (is_array($v)) { 301 foreach ($v as $val) { 302 $params[] = rawurlencode($p) . '[]=' . rawurlencode($val); 303 } 304 } else { 305 $params[] = rawurlencode($p) . 306 (strlen($v) ? ('=' . rawurlencode($v)) : ''); 307 } 308 } 309 310 $this->_cache = $params; 311 } 312 313 return $this->_cache; 314 } 315 316 /** 317 * Creates the full URL string. 318 * 319 * @return string The string representation of this object. 320 */ 321 public function __toString() 322 { 323 return $this->toString($this->raw); 324 } 325 326 /** 327 * Generates a HTML link tag out of this URL. 328 * 329 * @param array $attributes A hash with any additional attributes to be 330 * added to the link. If the attribute name is 331 * suffixed with ".raw", the attribute value 332 * won't be HTML-encoded. 333 * 334 * @return string An <a> tag representing this URL. 335 */ 336 public function link(array $attributes = array()) 337 { 338 $url = (string)$this->setRaw(false); 339 $link = '<a'; 340 if (!empty($url)) { 341 $link .= " href=\"$url\""; 342 } 343 foreach ($attributes as $name => $value) { 344 if (!strlen($value)) { 345 continue; 346 } 347 if (substr($name, -4) == '.raw') { 348 $link .= ' ' . htmlspecialchars(substr($name, 0, -4)) 349 . '="' . $value . '"'; 350 } else { 351 $link .= ' ' . htmlspecialchars($name) 352 . '="' . htmlspecialchars($value) . '"'; 353 } 354 } 355 return $link . '>'; 356 } 357 358 /** 359 * Add a unique parameter to the URL to aid in cache-busting. 360 * 361 * @return Horde_Url This (modified) object, to allow chaining. 362 */ 363 public function unique() 364 { 365 return $this->add('u', uniqid(mt_rand())); 366 } 367 368 /** 369 * Sends a redirect request to the browser to the URL in this object. 370 * 371 * @throws Horde_Url_Exception 372 */ 373 public function redirect() 374 { 375 $url = strval($this->setRaw(true)); 376 if (!strlen($url)) { 377 throw new Horde_Url_Exception('Redirect failed: URL is empty.'); 378 } 379 380 header('Location: ' . $url); 381 exit; 382 } 383 384 /** 385 * URL-safe base64 encoding, with trimmed '='. 386 * 387 * @param string $string String to encode. 388 * 389 * @return string URL-safe, base64 encoded data. 390 */ 391 public static function uriB64Encode($string) 392 { 393 return str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($string)); 394 } 395 396 /** 397 * Decode URL-safe base64 data, dealing with missing '='. 398 * 399 * @param string $string Encoded data. 400 * 401 * @return string Decoded data. 402 */ 403 public static function uriB64Decode($string) 404 { 405 $data = str_replace(array('-', '_'), array('+', '/'), $string); 406 $mod4 = strlen($data) % 4; 407 if ($mod4) { 408 $data .= substr('====', $mod4); 409 } 410 return base64_decode($data); 411 } 412 413} 414