1: <?php
2:
3: namespace PHPixie\HTTP\Messages;
4:
5: use InvalidArgumentException;
6: use Psr\Http\Message\UriInterface;
7:
8: 9: 10:
11: abstract class URI implements UriInterface
12: {
13: 14: 15:
16: const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
17:
18: 19: 20:
21: const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
22:
23: 24: 25: 26:
27: protected $uriString;
28:
29: 30: 31:
32: protected $parts = array();
33:
34: 35: 36:
37: public function __toString()
38: {
39: if ($this->uriString === null) {
40:
41: $uri = '';
42:
43: if(($scheme = $this->getScheme()) !== '') {
44: $uri .= $scheme . '://';
45: }
46:
47: $uri .= $this->getAuthority();
48: $uri .= $this->getPath();
49:
50: if(($query = $this->getQuery()) !== '') {
51: $uri .= '?' . $query;
52: }
53:
54: if(($fragment = $this->getFragment()) !== '') {
55: $uri .= '#' . $fragment;
56: }
57:
58: $this->uriString = $uri;
59: }
60:
61: return $this->uriString;
62: }
63:
64: 65: 66:
67: public function getAuthority()
68: {
69: $authority = '';
70: $authority.= $this->getHost();
71:
72: if (($userInfo = $this->getUserInfo()) !== '') {
73: $authority = $userInfo . '@' . $authority;
74: }
75:
76: $port = $this->getPort();
77: if ($port !== null) {
78: $authority .= ':' . $port;
79: }
80:
81: return $authority;
82: }
83:
84: 85: 86:
87: public function getScheme()
88: {
89: return $this->part('scheme');
90: }
91:
92: 93: 94:
95: public function getUserInfo()
96: {
97: return $this->part('userInfo');
98: }
99:
100: 101: 102:
103: public function getHost()
104: {
105: return $this->part('host');
106: }
107:
108: 109: 110:
111: public function getPort()
112: {
113: $port = $this->part('port');
114:
115: if($this->isStandardPort($this->getScheme(), $port)) {
116: $port = null;
117: }
118:
119: return $port;
120: }
121:
122: 123: 124:
125: public function getPath()
126: {
127: return $this->part('path');
128: }
129:
130: 131: 132:
133: public function getQuery()
134: {
135: return $this->part('query');
136: }
137:
138: 139: 140:
141: public function getFragment()
142: {
143: return $this->part('fragment');
144: }
145:
146: 147: 148:
149: public function withScheme($scheme)
150: {
151: $scheme = strtolower($scheme);
152: $scheme = str_replace('://', '', $scheme);
153:
154: if (!in_array($scheme, array('', 'http', 'https'), true)) {
155: throw new InvalidArgumentException("Unsupported scheme '$scheme', must be either 'http', 'https' or ''");
156: }
157:
158: return $this->updatePart('scheme', $scheme);
159: }
160:
161: 162: 163:
164: public function withUserInfo($user, $password = null)
165: {
166: $userInfo = $this->normalizePart($user);
167:
168: if ($userInfo !== '' && $password !== null) {
169: $userInfo.= ':' . $password;
170: }
171:
172: return $this->updatePart('userInfo', $userInfo);
173: }
174:
175: 176: 177: 178: 179:
180: protected function updatePart($key, $value)
181: {
182: $new = clone $this;
183: $new->parts[$key] = $value;
184: $new->uriString = null;
185:
186: return $new;
187: }
188:
189: 190: 191:
192: public function withHost($host)
193: {
194: $host = $this->normalizePart($host);
195: return $this->updatePart('host', $host);
196: }
197:
198: 199: 200:
201: public function withPort($port)
202: {
203: if($port !== null) {
204: if (!is_numeric($port)) {
205: throw new InvalidArgumentException("Port '$port' is not numeric");
206: }
207:
208: $port = (int) $port;
209:
210: if ($port < 1 || $port > 65535) {
211: throw new InvalidArgumentException("Invalid port '$port' specified");
212: }
213: }
214:
215: return $this->updatePart('port', $port);
216: }
217:
218: 219: 220:
221: public function withPath($path)
222: {
223: $path = $this->normalizePath($path);
224: return $this->updatePart('path', $path);
225: }
226:
227: 228: 229:
230: public function withQuery($query)
231: {
232: $query = $this->normalizeQuery($query);
233: return $this->updatePart('query', $query);
234:
235: }
236:
237: 238: 239:
240: public function withFragment($fragment)
241: {
242: $fragment = $this->normalizeFragment($fragment);
243: return $this->updatePart('fragment', $fragment);
244: }
245:
246: 247: 248: 249:
250: protected function normalizeFragment($fragment)
251: {
252: $fragment = $this->normalizePart($fragment, '#');
253: return $this->normalizeQueryString($fragment);
254: }
255:
256: 257: 258: 259: 260:
261: protected function normalizePart($part, $prefix = null) {
262: if($part === null || $part === '') {
263: return '';
264: }
265:
266: if($prefix !== null && $part[0] === $prefix) {
267: return substr($part, 1);
268: }
269:
270: return $part;
271: }
272:
273: 274: 275: 276: 277:
278: protected static function isStandardPort($scheme, $port)
279: {
280: if ($scheme === 'https' && $port === 443) {
281: return true;
282: }
283:
284: if ($scheme === 'http' && $port === 80) {
285: return true;
286: }
287:
288: return false;
289: }
290:
291: 292: 293: 294:
295: protected function normalizePath($path)
296: {
297: if (strpos($path, '?') !== false) {
298: throw new InvalidArgumentException("Path '$path' contains '?'");
299: }
300:
301: if (strpos($path, '#') !== false) {
302: throw new InvalidArgumentException("Path '$path' contains '#'");
303: }
304:
305: if ($path === null || $path === '') {
306: return '/';
307: }
308:
309: if($path[0] !== '/') {
310: $path = '/' . $path;
311: }
312:
313: return preg_replace_callback(
314: '/(?:[^' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
315: array($this, 'encodeMatchedQueryPart'),
316: $path
317: );
318: }
319:
320: 321: 322: 323: 324:
325: protected function normalizeQuery($query)
326: {
327: if (strpos($query, '#') !== false) {
328: throw new InvalidArgumentException(
329: 'Query string must not include a URI fragment'
330: );
331: }
332:
333: $query = $this->normalizePart($query, '?');
334:
335: $pairs = explode('&', $query);
336:
337: foreach ($pairs as $pairKey => $pair) {
338: $pair = explode('=', $pair, 2);
339: foreach($pair as $key => $value) {
340: $pair[$key] = $this->normalizeQueryString($value);
341: }
342:
343: $parts[$pairKey] = implode('=', $pair);
344: }
345:
346: return implode('&', $parts);
347: }
348:
349: 350: 351: 352:
353: protected function normalizeQueryString($value)
354: {
355: return preg_replace_callback(
356: '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
357: array($this, 'encodeMatchedQueryPart'),
358: $value
359: );
360: }
361:
362: 363: 364: 365:
366: protected function encodeMatchedQueryPart($matches) {
367: return rawurlencode($matches[0]);
368: }
369:
370: 371: 372: 373:
374: protected function part($name)
375: {
376: if(!array_key_exists($name, $this->parts)) {
377: $this->requirePart($name);
378: }
379:
380: return $this->parts[$name];
381: }
382:
383: 384: 385:
386: protected function requirePart($name)
387: {
388: if($name === 'port') {
389: $this->parts['port'] = null;
390: }else{
391: $this->parts[$name] = '';
392: }
393: }
394: }
395: