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, '&amp;') !== 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 ? '&' : '&amp;', $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