1<?php
2
3namespace OAuth\Common\Http\Uri;
4
5use InvalidArgumentException;
6
7/**
8 * Standards-compliant URI class.
9 */
10class Uri implements UriInterface
11{
12    /**
13     * @var string
14     */
15    private $scheme = 'http';
16
17    /**
18     * @var string
19     */
20    private $userInfo = '';
21
22    /**
23     * @var string
24     */
25    private $rawUserInfo = '';
26
27    /**
28     * @var string
29     */
30    private $host;
31
32    /**
33     * @var int
34     */
35    private $port = 80;
36
37    /**
38     * @var string
39     */
40    private $path = '/';
41
42    /**
43     * @var string
44     */
45    private $query = '';
46
47    /**
48     * @var string
49     */
50    private $fragment = '';
51
52    /**
53     * @var bool
54     */
55    private $explicitPortSpecified = false;
56
57    /**
58     * @var bool
59     */
60    private $explicitTrailingHostSlash = false;
61
62    /**
63     * @param string $uri
64     */
65    public function __construct($uri = null)
66    {
67        if (null !== $uri) {
68            $this->parseUri($uri);
69        }
70    }
71
72    /**
73     * @param string $uri
74     *
75     * @throws \InvalidArgumentException
76     */
77    protected function parseUri($uri)
78    {
79        if (false === ($uriParts = parse_url($uri))) {
80            // congratulations if you've managed to get parse_url to fail,
81            // it seems to always return some semblance of a parsed url no matter what
82            throw new InvalidArgumentException("Invalid URI: $uri");
83        }
84
85        if (!isset($uriParts['scheme'])) {
86            throw new InvalidArgumentException('Invalid URI: http|https scheme required');
87        }
88
89        $this->scheme = $uriParts['scheme'];
90        $this->host = $uriParts['host'];
91
92        if (isset($uriParts['port'])) {
93            $this->port = $uriParts['port'];
94            $this->explicitPortSpecified = true;
95        } else {
96            $this->port = strcmp('https', $uriParts['scheme']) ? 80 : 443;
97            $this->explicitPortSpecified = false;
98        }
99
100        if (isset($uriParts['path'])) {
101            $this->path = $uriParts['path'];
102            if ('/' === $uriParts['path']) {
103                $this->explicitTrailingHostSlash = true;
104            }
105        } else {
106            $this->path = '/';
107        }
108
109        $this->query = isset($uriParts['query']) ? $uriParts['query'] : '';
110        $this->fragment = isset($uriParts['fragment']) ? $uriParts['fragment'] : '';
111
112        $userInfo = '';
113        if (!empty($uriParts['user'])) {
114            $userInfo .= $uriParts['user'];
115        }
116        if ($userInfo && !empty($uriParts['pass'])) {
117            $userInfo .= ':' . $uriParts['pass'];
118        }
119
120        $this->setUserInfo($userInfo);
121    }
122
123    /**
124     * @param string $rawUserInfo
125     *
126     * @return string
127     */
128    protected function protectUserInfo($rawUserInfo)
129    {
130        $colonPos = strpos($rawUserInfo, ':');
131
132        // rfc3986-3.2.1 | http://tools.ietf.org/html/rfc3986#section-3.2
133        // "Applications should not render as clear text any data
134        // after the first colon (":") character found within a userinfo
135        // subcomponent unless the data after the colon is the empty string
136        // (indicating no password)"
137        if ($colonPos !== false && strlen($rawUserInfo)-1 > $colonPos) {
138            return substr($rawUserInfo, 0, $colonPos) . ':********';
139        } else {
140            return $rawUserInfo;
141        }
142    }
143
144    /**
145     * @return string
146     */
147    public function getScheme()
148    {
149        return $this->scheme;
150    }
151
152    /**
153     * @return string
154     */
155    public function getUserInfo()
156    {
157        return $this->userInfo;
158    }
159
160    /**
161     * @return string
162     */
163    public function getRawUserInfo()
164    {
165        return $this->rawUserInfo;
166    }
167
168    /**
169     * @return string
170     */
171    public function getHost()
172    {
173        return $this->host;
174    }
175
176    /**
177     * @return int
178     */
179    public function getPort()
180    {
181        return $this->port;
182    }
183
184    /**
185     * @return string
186     */
187    public function getPath()
188    {
189        return $this->path;
190    }
191
192    /**
193     * @return string
194     */
195    public function getQuery()
196    {
197        return $this->query;
198    }
199
200    /**
201     * @return string
202     */
203    public function getFragment()
204    {
205        return $this->fragment;
206    }
207
208    /**
209     * Uses protected user info by default as per rfc3986-3.2.1
210     * Uri::getRawAuthority() is available if plain-text password information is desirable.
211     *
212     * @return string
213     */
214    public function getAuthority()
215    {
216        $authority = $this->userInfo ? $this->userInfo.'@' : '';
217        $authority .= $this->host;
218
219        if ($this->explicitPortSpecified) {
220            $authority .= ":{$this->port}";
221        }
222
223        return $authority;
224    }
225
226    /**
227     * @return string
228     */
229    public function getRawAuthority()
230    {
231        $authority = $this->rawUserInfo ? $this->rawUserInfo.'@' : '';
232        $authority .= $this->host;
233
234        if ($this->explicitPortSpecified) {
235            $authority .= ":{$this->port}";
236        }
237
238        return $authority;
239    }
240
241    /**
242     * @return string
243     */
244    public function getAbsoluteUri()
245    {
246        $uri = $this->scheme . '://' . $this->getRawAuthority();
247
248        if ('/' === $this->path) {
249            $uri .= $this->explicitTrailingHostSlash ? '/' : '';
250        } else {
251            $uri .= $this->path;
252        }
253
254        if (!empty($this->query)) {
255            $uri .= "?{$this->query}";
256        }
257
258        if (!empty($this->fragment)) {
259            $uri .= "#{$this->fragment}";
260        }
261
262        return $uri;
263    }
264
265    /**
266     * @return string
267     */
268    public function getRelativeUri()
269    {
270        $uri = '';
271
272        if ('/' === $this->path) {
273            $uri .= $this->explicitTrailingHostSlash ? '/' : '';
274        } else {
275            $uri .= $this->path;
276        }
277
278        return $uri;
279    }
280
281    /**
282     * Uses protected user info by default as per rfc3986-3.2.1
283     * Uri::getAbsoluteUri() is available if plain-text password information is desirable.
284     *
285     * @return string
286     */
287    public function __toString()
288    {
289        $uri = $this->scheme . '://' . $this->getAuthority();
290
291        if ('/' === $this->path) {
292            $uri .= $this->explicitTrailingHostSlash ? '/' : '';
293        } else {
294            $uri .= $this->path;
295        }
296
297        if (!empty($this->query)) {
298            $uri .= "?{$this->query}";
299        }
300
301        if (!empty($this->fragment)) {
302            $uri .= "#{$this->fragment}";
303        }
304
305        return $uri;
306    }
307
308    /**
309     * @param $path
310     */
311    public function setPath($path)
312    {
313        if (empty($path)) {
314            $this->path = '/';
315            $this->explicitTrailingHostSlash = false;
316        } else {
317            $this->path = $path;
318            if ('/' === $this->path) {
319                $this->explicitTrailingHostSlash = true;
320            }
321        }
322    }
323
324    /**
325     * @param string $query
326     */
327    public function setQuery($query)
328    {
329        $this->query = $query;
330    }
331
332    /**
333     * @param string $var
334     * @param string $val
335     */
336    public function addToQuery($var, $val)
337    {
338        if (strlen($this->query) > 0) {
339            $this->query .= '&';
340        }
341        $this->query .= http_build_query(array($var => $val), '', '&');
342    }
343
344    /**
345     * @param string $fragment
346     */
347    public function setFragment($fragment)
348    {
349        $this->fragment = $fragment;
350    }
351
352    /**
353     * @param string $scheme
354     */
355    public function setScheme($scheme)
356    {
357        $this->scheme = $scheme;
358    }
359
360
361    /**
362     * @param string $userInfo
363     */
364    public function setUserInfo($userInfo)
365    {
366        $this->userInfo = $userInfo ? $this->protectUserInfo($userInfo) : '';
367        $this->rawUserInfo = $userInfo;
368    }
369
370
371    /**
372     * @param int $port
373     */
374    public function setPort($port)
375    {
376        $this->port = intval($port);
377
378        if (('https' === $this->scheme && $this->port === 443) || ('http' === $this->scheme && $this->port === 80)) {
379            $this->explicitPortSpecified = false;
380        } else {
381            $this->explicitPortSpecified = true;
382        }
383    }
384
385    /**
386     * @param string $host
387     */
388    public function setHost($host)
389    {
390        $this->host = $host;
391    }
392
393    /**
394     * @return bool
395     */
396    public function hasExplicitTrailingHostSlash()
397    {
398        return $this->explicitTrailingHostSlash;
399    }
400
401    /**
402     * @return bool
403     */
404    public function hasExplicitPortSpecified()
405    {
406        return $this->explicitPortSpecified;
407    }
408}
409