1: <?php
2: /**
3: * This file contains the Horde_Url class for manipulating URLs.
4: *
5: * Copyright 2009-2012 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: */
26: class Horde_Url
27: {
28: /**
29: * The basic URL, without query parameters.
30: *
31: * @var string
32: */
33: public $url;
34:
35: /**
36: * Whether to output the URL in the raw URL format or HTML-encoded.
37: *
38: * @var boolean
39: */
40: public $raw;
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: * Any PATH_INFO to be added to the URL.
54: *
55: * @var string
56: */
57: public $pathInfo;
58:
59: /**
60: * The anchor string.
61: *
62: * @var string
63: */
64: public $anchor = '';
65:
66: /**
67: * A callback function to use when converting to a string.
68: *
69: * @var callback
70: */
71: public $toStringCallback;
72:
73: /**
74: * Constructor.
75: *
76: * @param string $url The basic URL, with or without query parameters.
77: * @param boolean $raw Whether to output the URL in the raw URL format or
78: * HTML-encoded.
79: */
80: public function __construct($url, $raw = null)
81: {
82: if ($url instanceof Horde_Url) {
83: $this->anchor = $url->anchor;
84: $this->parameters = $url->parameters;
85: $this->pathInfo = $url->pathInfo;
86: $this->raw = is_null($raw) ? $url->raw : $raw;
87: $this->url = $url->url;
88: return;
89: }
90:
91: if (($pos = strrpos($url, '#')) !== false) {
92: $this->anchor = urldecode(substr($url, $pos + 1));
93: $url = substr($url, 0, $pos);
94: }
95:
96: if (($pos = strrpos($url, '?')) !== false) {
97: $query = substr($url, $pos + 1);
98: $url = substr($url, 0, $pos);
99:
100: /* Check if the argument separator has been already
101: * htmlentities-ized in the URL. */
102: if (preg_match('/&/', $query)) {
103: $query = html_entity_decode($query);
104: if (is_null($raw)) {
105: $raw = false;
106: }
107: } elseif (preg_match('/&/', $query)) {
108: if (is_null($raw)) {
109: $raw = true;
110: }
111: }
112: $pairs = explode('&', $query);
113: foreach ($pairs as $pair) {
114: $result = explode('=', urldecode($pair), 2);
115: $this->add($result[0], isset($result[1]) ? $result[1] : null);
116: }
117: }
118:
119: $this->url = $url;
120: $this->raw = $raw;
121: }
122:
123: /**
124: * Returns a clone of this object. Useful for chaining.
125: *
126: * @return Horde_Url A clone of this object.
127: */
128: public function copy()
129: {
130: $url = clone $this;
131: return $url;
132: }
133:
134: /**
135: * Adds one or more query parameters.
136: *
137: * @param mixed $parameters Either the name value or an array of
138: * name/value pairs.
139: * @param string $value If specified, the value part ($parameters is
140: * then assumed to just be the parameter name).
141: *
142: * @return Horde_Url This (modified) object, to allow chaining.
143: */
144: public function add($parameters, $value = null)
145: {
146: if (!is_array($parameters)) {
147: $parameters = array($parameters => $value);
148: }
149:
150: foreach ($parameters as $parameter => $value) {
151: if (substr($parameter, -2) == '[]') {
152: $parameter = substr($parameter, 0, -2);
153: if (!isset($this->parameters[$parameter])) {
154: $this->parameters[$parameter] = array();
155: }
156: $this->parameters[$parameter][] = $value;
157: } else {
158: $this->parameters[$parameter] = $value;
159: }
160: }
161:
162: return $this;
163: }
164:
165: /**
166: * Removes one ore more parameters.
167: *
168: * @param mixed $remove Either a single parameter to remove or an array
169: * of parameters to remove.
170: *
171: * @return Horde_Url This (modified) object, to allow chaining.
172: */
173: public function remove($parameters)
174: {
175: if (!is_array($parameters)) {
176: $parameters = array($parameters);
177: }
178:
179: foreach ($parameters as $parameter) {
180: unset($this->parameters[$parameter]);
181: }
182:
183: return $this;
184: }
185:
186: /**
187: * Sets the URL anchor.
188: *
189: * @param string $anchor An anchor to add.
190: *
191: * @return Horde_Url This (modified) object, to allow chaining.
192: */
193: public function setAnchor($anchor)
194: {
195: $this->anchor = $anchor;
196: return $this;
197: }
198:
199: /**
200: * Sets the $raw value. This call can be chained.
201: *
202: * @param boolean $raw Whether to output the URL in the raw URL format or
203: * HTML-encoded.
204: *
205: * @return Horde_Url This object, to allow chaining.
206: */
207: public function setRaw($raw)
208: {
209: $this->raw = $raw;
210: return $this;
211: }
212:
213: /**
214: * Creates the full URL string.
215: *
216: * @param boolean $raw Whether to output the URL in the raw URL format
217: * or HTML-encoded.
218: * @param boolean $full Output the full URL?
219: *
220: * @return string The string representation of this object.
221: */
222: public function toString($raw = false, $full = true)
223: {
224: if ($this->toStringCallback) {
225: $callback = $this->toStringCallback;
226: $this->toStringCallback = null;
227: $ret = call_user_func($callback, $this);
228: $this->toStringCallback = $callback;
229: return $ret;
230: }
231:
232: $url_params = array();
233: foreach ($this->parameters as $parameter => $value) {
234: if (is_array($value)) {
235: foreach ($value as $val) {
236: $url_params[] = rawurlencode($parameter) . '[]=' . rawurlencode($val);
237: }
238: } else {
239: if (strlen($value)) {
240: $url_params[] = rawurlencode($parameter) . '=' . rawurlencode($value);
241: } else {
242: $url_params[] = rawurlencode($parameter);
243: }
244: }
245: }
246:
247: $url = $full
248: ? $this->url
249: : parse_url($this->url, PHP_URL_PATH);
250:
251: if (strlen($this->pathInfo)) {
252: $url = rtrim($url, '/');
253: $url .= '/' . $this->pathInfo;
254: }
255: if (count($url_params)) {
256: $url .= '?' . implode($raw ? '&' : '&', $url_params);
257: }
258: if ($this->anchor) {
259: $url .= '#' . ($raw ? $this->anchor : rawurlencode($this->anchor));
260: }
261:
262: return strval($url);
263: }
264:
265: /**
266: * Creates the full URL string.
267: *
268: * @return string The string representation of this object.
269: */
270: public function __toString()
271: {
272: return $this->toString($this->raw);
273: }
274:
275: /**
276: * Generates a HTML link tag out of this URL.
277: *
278: * @param array $attributes A hash with any additional attributes to be
279: * added to the link. If the attribute name is
280: * suffixed with ".raw", the attribute value
281: * won't be HTML-encoded.
282: *
283: * @return string An <a> tag representing this URL.
284: */
285: public function link(array $attributes = array())
286: {
287: $url = (string)$this->setRaw(false);
288: $link = '<a';
289: if (!empty($url)) {
290: $link .= " href=\"$url\"";
291: }
292: foreach ($attributes as $name => $value) {
293: if (!strlen($value)) {
294: continue;
295: }
296: if (substr($name, -4) == '.raw') {
297: $link .= ' ' . htmlspecialchars(substr($name, 0, -4))
298: . '="' . $value . '"';
299: } else {
300: $link .= ' ' . htmlspecialchars($name)
301: . '="' . htmlspecialchars($value) . '"';
302: }
303: }
304: return $link . '>';
305: }
306:
307: /**
308: * Add a unique parameter to the URL to aid in cache-busting.
309: *
310: * @return Horde_Url This (modified) object, to allow chaining.
311: */
312: public function unique()
313: {
314: return $this->add('u', uniqid(mt_rand()));
315: }
316:
317: /**
318: * Sends a redirect request to the browser to the URL in this object.
319: *
320: * @throws Horde_Url_Exception
321: */
322: public function redirect()
323: {
324: $url = strval($this->setRaw(true));
325: if (!strlen($url)) {
326: throw new Horde_Url_Exception('Redirect failed: URL is empty.');
327: }
328:
329: header('Location: ' . $url);
330: exit;
331: }
332:
333: /**
334: * URL-safe base64 encoding, with trimmed '='.
335: *
336: * @param string $string String to encode.
337: *
338: * @return string URL-safe, base64 encoded data.
339: */
340: static public function uriB64Encode($string)
341: {
342: return str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($string));
343: }
344:
345: /**
346: * Decode URL-safe base64 data, dealing with missing '='.
347: *
348: * @param string $string Encoded data.
349: *
350: * @return string Decoded data.
351: */
352: static public function uriB64Decode($string)
353: {
354: $data = str_replace(array('-', '_'), array('+', '/'), $string);
355: $mod4 = strlen($data) % 4;
356: if ($mod4) {
357: $data .= substr('====', $mod4);
358: }
359: return base64_decode($data);
360: }
361:
362: }
363: