1<?php 2 3namespace Mpdf\PsrHttpMessageShim; 4 5use Psr\Http\Message\UriInterface; 6 7/** 8 * PSR-7 URI implementation ported from nyholm/psr7 and adapted for PHP 5.6 9 * 10 * @link https://github.com/Nyholm/psr7/blob/master/src/Uri.php 11 */ 12final class Uri implements UriInterface 13{ 14 private static $schemes = ['http' => 80, 'https' => 443]; 15 16 const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; 17 18 const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; 19 20 /** @var string Uri scheme. */ 21 private $scheme = ''; 22 23 /** @var string Uri user info. */ 24 private $userInfo = ''; 25 26 /** @var string Uri host. */ 27 private $host = ''; 28 29 /** @var int|null Uri port. */ 30 private $port; 31 32 /** @var string Uri path. */ 33 private $path = ''; 34 35 /** @var string Uri query string. */ 36 private $query = ''; 37 38 /** @var string Uri fragment. */ 39 private $fragment = ''; 40 41 public function __construct($uri = '') 42 { 43 if ('' !== $uri) { 44 if (false === $parts = \parse_url($uri)) { 45 throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); 46 } 47 48 // Apply parse_url parts to a URI. 49 $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; 50 $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; 51 $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; 52 $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; 53 $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; 54 $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; 55 $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; 56 if (isset($parts['pass'])) { 57 $this->userInfo .= ':' . $parts['pass']; 58 } 59 } 60 } 61 62 public function __toString(): string 63 { 64 return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); 65 } 66 67 public function getScheme(): string 68 { 69 return $this->scheme; 70 } 71 72 public function getAuthority(): string 73 { 74 if ('' === $this->host) { 75 return ''; 76 } 77 78 $authority = $this->host; 79 if ('' !== $this->userInfo) { 80 $authority = $this->userInfo . '@' . $authority; 81 } 82 83 if (null !== $this->port) { 84 $authority .= ':' . $this->port; 85 } 86 87 return $authority; 88 } 89 90 public function getUserInfo(): string 91 { 92 return $this->userInfo; 93 } 94 95 public function getHost(): string 96 { 97 return $this->host; 98 } 99 100 public function getPort(): ?int 101 { 102 return $this->port; 103 } 104 105 public function getPath(): string 106 { 107 return $this->path; 108 } 109 110 public function getQuery(): string 111 { 112 return $this->query; 113 } 114 115 public function getFragment(): string 116 { 117 return $this->fragment; 118 } 119 120 public function withScheme(string $scheme): UriInterface 121 { 122 if (!\is_string($scheme)) { 123 throw new \InvalidArgumentException('Scheme must be a string'); 124 } 125 126 if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { 127 return $this; 128 } 129 130 $new = clone $this; 131 $new->scheme = $scheme; 132 $new->port = $new->filterPort($new->port); 133 134 return $new; 135 } 136 137 public function withUserInfo(string $user, ?string $password = null): UriInterface 138 { 139 $info = $user; 140 if (null !== $password && '' !== $password) { 141 $info .= ':' . $password; 142 } 143 144 if ($this->userInfo === $info) { 145 return $this; 146 } 147 148 $new = clone $this; 149 $new->userInfo = $info; 150 151 return $new; 152 } 153 154 public function withHost(string $host): UriInterface 155 { 156 if (!\is_string($host)) { 157 throw new \InvalidArgumentException('Host must be a string'); 158 } 159 160 if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { 161 return $this; 162 } 163 164 $new = clone $this; 165 $new->host = $host; 166 167 return $new; 168 } 169 170 public function withPort(?int $port): UriInterface 171 { 172 if ($this->port === $port = $this->filterPort($port)) { 173 return $this; 174 } 175 176 $new = clone $this; 177 $new->port = $port; 178 179 return $new; 180 } 181 182 public function withPath(string $path): UriInterface 183 { 184 if ($this->path === $path = $this->filterPath($path)) { 185 return $this; 186 } 187 188 $new = clone $this; 189 $new->path = $path; 190 191 return $new; 192 } 193 194 public function withQuery(string $query): UriInterface 195 { 196 if ($this->query === $query = $this->filterQueryAndFragment($query)) { 197 return $this; 198 } 199 200 $new = clone $this; 201 $new->query = $query; 202 203 return $new; 204 } 205 206 public function withFragment(string $fragment): UriInterface 207 { 208 if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { 209 return $this; 210 } 211 212 $new = clone $this; 213 $new->fragment = $fragment; 214 215 return $new; 216 } 217 218 /** 219 * Create a URI string from its various parts. 220 */ 221 private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string 222 { 223 $uri = ''; 224 if ('' !== $scheme) { 225 $uri .= $scheme . ':'; 226 } 227 228 if ('' !== $authority) { 229 $uri .= '//' . $authority; 230 } 231 232 if ('' !== $path) { 233 if ('/' !== $path[0]) { 234 if ('' !== $authority) { 235 // If the path is rootless and an authority is present, the path MUST be prefixed by "/" 236 $path = '/' . $path; 237 } 238 } elseif (isset($path[1]) && '/' === $path[1]) { 239 if ('' === $authority) { 240 // If the path is starting with more than one "/" and no authority is present, the 241 // starting slashes MUST be reduced to one. 242 $path = '/' . \ltrim($path, '/'); 243 } 244 } 245 246 $uri .= $path; 247 } 248 249 if ('' !== $query) { 250 $uri .= '?' . $query; 251 } 252 253 if ('' !== $fragment) { 254 $uri .= '#' . $fragment; 255 } 256 257 return $uri; 258 } 259 260 /** 261 * Is a given port non-standard for the current scheme? 262 */ 263 private static function isNonStandardPort(string $scheme, int $port): bool 264 { 265 return !isset(self::$schemes[$scheme]) || $port !== self::$schemes[$scheme]; 266 } 267 268 private function filterPort(int $port): ?int 269 { 270 if (null === $port) { 271 return null; 272 } 273 274 $port = (int) $port; 275 if (0 > $port || 0xffff < $port) { 276 throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); 277 } 278 279 return self::isNonStandardPort($this->scheme, $port) ? $port : null; 280 } 281 282 private function filterPath($path) 283 { 284 if (!\is_string($path)) { 285 throw new \InvalidArgumentException('Path must be a string'); 286 } 287 288 return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); 289 } 290 291 private function filterQueryAndFragment($str) 292 { 293 if (!\is_string($str)) { 294 throw new \InvalidArgumentException('Query and fragment must be a string'); 295 } 296 297 return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); 298 } 299 300 private static function rawurlencodeMatchZero(array $match) 301 { 302 return \rawurlencode($match[0]); 303 } 304 305} 306