diff --git a/composer.json b/composer.json index d5a609cc..0c419de4 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index 4afd6656..eafee1ca 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -42,13 +42,16 @@ final class FileUpload private readonly int $error; - public function __construct(?array $value) + public function __construct(array|string|null $value) { - foreach (['name', 'size', 'tmp_name', 'error'] as $key) { - if (!isset($value[$key]) || !is_scalar($value[$key])) { - $value = []; - break; - } + if (is_string($value)) { + $value = [ + 'name' => basename($value), + 'full_path' => $value, + 'size' => filesize($value), + 'tmp_name' => $value, + 'error' => UPLOAD_ERR_OK, + ]; } $this->name = $value['name'] ?? ''; @@ -64,6 +67,7 @@ public function __construct(?array $value) */ public function getName(): string { + trigger_error(__METHOD__ . '() is deprecated, use getUntrustedName()', E_USER_DEPRECATED); return $this->name; } diff --git a/src/Http/Helpers.php b/src/Http/Helpers.php index f4b5fe78..807c8d64 100644 --- a/src/Http/Helpers.php +++ b/src/Http/Helpers.php @@ -23,9 +23,6 @@ final class Helpers /** @internal */ public const StrictCookieName = '_nss'; - /** @deprecated */ - public const STRICT_COOKIE_NAME = self::StrictCookieName; - /** * Returns HTTP valid date format. diff --git a/src/Http/IRequest.php b/src/Http/IRequest.php index 9c3fb8f7..434682c1 100644 --- a/src/Http/IRequest.php +++ b/src/Http/IRequest.php @@ -58,22 +58,19 @@ function getUrl(): UrlScript; /** * Returns variable provided to the script via URL query ($_GET). * If no key is passed, returns the entire array. - * @return mixed */ - function getQuery(?string $key = null); + function getQuery(?string $key = null): mixed; /** * Returns variable provided to the script via POST method ($_POST). * If no key is passed, returns the entire array. - * @return mixed */ - function getPost(?string $key = null); + function getPost(?string $key = null): mixed; /** * Returns uploaded file. - * @return FileUpload|array|null */ - function getFile(string $key); + function getFile(string $key): ?FileUpload; /** * Returns uploaded files. @@ -82,9 +79,8 @@ function getFiles(): array; /** * Returns variable provided to the script via HTTP cookies. - * @return mixed */ - function getCookie(string $key); + function getCookie(string $key): mixed; /** * Returns variables provided to the script via HTTP cookies. diff --git a/src/Http/IResponse.php b/src/Http/IResponse.php index b71dfe29..f3e64844 100644 --- a/src/Http/IResponse.php +++ b/src/Http/IResponse.php @@ -337,9 +337,8 @@ interface IResponse /** * Sets HTTP response code. - * @return static */ - function setCode(int $code, ?string $reason = null); + function setCode(int $code, ?string $reason = null): static; /** * Returns HTTP response code. @@ -348,21 +347,18 @@ function getCode(): int; /** * Sends a HTTP header and replaces a previous one. - * @return static */ - function setHeader(string $name, string $value); + function setHeader(string $name, string $value): static; /** * Adds HTTP header. - * @return static */ - function addHeader(string $name, string $value); + function addHeader(string $name, string $value): static; /** * Sends a Content-type HTTP header. - * @return static */ - function setContentType(string $type, ?string $charset = null); + function setContentType(string $type, ?string $charset = null): static; /** * Redirects to a new URL. @@ -371,9 +367,8 @@ function redirect(string $url, int $code = self::S302_Found): void; /** * Sets the time (like '20 minutes') before a page cached on a browser expires, null means "must-revalidate". - * @return static */ - function setExpiration(?string $expire); + function setExpiration(?string $expire): static; /** * Checks if headers have been sent. @@ -392,7 +387,6 @@ function getHeaders(): array; /** * Sends a cookie. - * @return static */ function setCookie( string $name, @@ -400,12 +394,18 @@ function setCookie( string|int|\DateTimeInterface|null $expire, ?string $path = null, ?string $domain = null, - ?bool $secure = null, - ?bool $httpOnly = null, - ); + bool $secure = false, + bool $httpOnly = true, + string $sameSite = self::SameSiteLax, + ): static; /** * Deletes a cookie. */ - function deleteCookie(string $name, ?string $path = null, ?string $domain = null, ?bool $secure = null); + function deleteCookie( + string $name, + ?string $path = null, + ?string $domain = null, + bool $secure = false, + ); } diff --git a/src/Http/Request.php b/src/Http/Request.php index 551a05b9..b34b30c4 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -189,6 +189,7 @@ public function getHeaders(): array */ public function getReferer(): ?UrlImmutable { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return isset($this->headers['referer']) ? new UrlImmutable($this->headers['referer']) : null; @@ -252,10 +253,6 @@ public function getRemoteAddress(): ?string */ public function getRemoteHost(): ?string { - if ($this->remoteHost === null && $this->remoteAddress !== null) { - $this->remoteHost = gethostbyaddr($this->remoteAddress); - } - return $this->remoteHost; } @@ -269,6 +266,20 @@ public function getRawBody(): ?string } + /** + * Returns decoded content of HTTP request body. + */ + public function getDecodedBody(): mixed + { + $type = $this->getHeader('Content-Type'); + return match ($type) { + 'application/json' => json_decode($this->getRawBody()), + 'application/x-www-form-urlencoded' => $_POST, + default => throw new \Exception("Unsupported content type: $type"), + }; + } + + /** * Returns basic HTTP authentication credentials. * @return array{string, string}|null diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index f44490e7..5f8210e7 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -374,9 +374,10 @@ private function parseHostAndPort(string $s): ?array } - /** @deprecated */ + /** @deprecated use fromGlobals() */ public function createHttpRequest(): Request { + trigger_error(__METHOD__ . '() is deprecated, use fromGlobals()', E_USER_DEPRECATED); return $this->fromGlobals(); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index 86de0b4c..65725da5 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -233,8 +233,8 @@ public function setCookie( ?string $path = null, ?string $domain = null, ?bool $secure = null, - ?bool $httpOnly = null, - ?string $sameSite = null, + bool $httpOnly = true, + string $sameSite = self::SameSiteLax, ): static { self::checkHeaders(); @@ -243,8 +243,8 @@ public function setCookie( 'path' => $path ?? ($domain ? '/' : $this->cookiePath), 'domain' => $domain ?? ($path ? '' : $this->cookieDomain), 'secure' => $secure ?? $this->cookieSecure, - 'httponly' => $httpOnly ?? true, - 'samesite' => $sameSite ?? self::SameSiteLax, + 'httponly' => $httpOnly, + 'samesite' => $sameSite, ]); return $this; } diff --git a/src/Http/SessionSection.php b/src/Http/SessionSection.php index f79b3025..e117df27 100644 --- a/src/Http/SessionSection.php +++ b/src/Http/SessionSection.php @@ -17,9 +17,6 @@ */ class SessionSection implements \IteratorAggregate, \ArrayAccess { - public bool $warnOnUndefined = false; - - /** * Do not call directly. Use Session::getSection(). */ @@ -97,6 +94,7 @@ public function remove(string|array|null $name = null): void */ public function __set(string $name, $value): void { + trigger_error("Writing to \$session->$name is deprecated, use \$session->set('$name', \$value) instead", E_USER_DEPRECATED); $this->session->autoStart(true); $this->getData()[$name] = $value; } @@ -108,12 +106,9 @@ public function __set(string $name, $value): void */ public function &__get(string $name): mixed { + trigger_error("Reading from \$session->$name is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED); $this->session->autoStart(true); $data = &$this->getData(); - if ($this->warnOnUndefined && !array_key_exists($name, $data ?? [])) { - trigger_error("The variable '$name' does not exist in session section"); - } - return $data[$name]; } @@ -124,6 +119,7 @@ public function &__get(string $name): mixed */ public function __isset(string $name): bool { + trigger_error("Using \$session->$name is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED); $this->session->autoStart(false); return isset($this->getData()[$name]); } @@ -135,6 +131,7 @@ public function __isset(string $name): bool */ public function __unset(string $name): void { + trigger_error("Unset(\$session->$name) is deprecated, use \$session->remove('$name') instead", E_USER_DEPRECATED); $this->remove($name); } @@ -145,6 +142,7 @@ public function __unset(string $name): void */ public function offsetSet($name, $value): void { + trigger_error("Writing to \$session['$name'] is deprecated, use \$session->set('$name', \$value) instead", E_USER_DEPRECATED); $this->__set($name, $value); } @@ -155,6 +153,7 @@ public function offsetSet($name, $value): void */ public function offsetGet($name): mixed { + trigger_error("Reading from \$session['$name'] is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED); return $this->get($name); } @@ -165,6 +164,7 @@ public function offsetGet($name): mixed */ public function offsetExists($name): bool { + trigger_error("Using \$session['$name'] is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED); return $this->__isset($name); } @@ -175,6 +175,7 @@ public function offsetExists($name): bool */ public function offsetUnset($name): void { + trigger_error("Unset(\$session['$name']) is deprecated, use \$session->remove('$name') instead", E_USER_DEPRECATED); $this->remove($name); } diff --git a/src/Http/Url.php b/src/Http/Url.php index 15176668..45f2ff31 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -100,28 +100,36 @@ public function getScheme(): string } + /** @deprecated */ public function setUser(string $user): static { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); $this->user = $user; return $this; } + /** @deprecated */ public function getUser(): string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->user; } + /** @deprecated */ public function setPassword(string $password): static { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); $this->password = $password; return $this; } + /** @deprecated */ public function getPassword(): string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->password; } @@ -145,7 +153,7 @@ public function getHost(): string */ public function getDomain(int $level = 2): string { - $parts = ip2long($this->host) + $parts = filter_var($this->host, FILTER_VALIDATE_IP) ? [$this->host] : explode('.', $this->host); $parts = $level >= 0 @@ -283,6 +291,7 @@ public function getHostUrl(): string /** @deprecated use UrlScript::getBasePath() instead */ public function getBasePath(): string { + trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED); $pos = strrpos($this->path, '/'); return $pos === false ? '' : substr($this->path, 0, $pos + 1); } @@ -291,6 +300,7 @@ public function getBasePath(): string /** @deprecated use UrlScript::getBaseUrl() instead */ public function getBaseUrl(): string { + trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED); return $this->getHostUrl() . $this->getBasePath(); } @@ -298,6 +308,7 @@ public function getBaseUrl(): string /** @deprecated use UrlScript::getRelativeUrl() instead */ public function getRelativeUrl(): string { + trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED); return substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl())); } @@ -332,7 +343,7 @@ public function isEqual(string|self|UrlImmutable $url): bool public function canonicalize(): static { $this->path = preg_replace_callback( - '#[^!$&\'()*+,/:;=@%]+#', + '#[^!$&\'()*+,/:;=@%"]+#', fn(array $m): string => rawurlencode($m[0]), self::unescape($this->path, '%/'), ); @@ -409,4 +420,34 @@ public static function parseQuery(string $s): array parse_str($s, $res); return $res[0] ?? []; } + + + public static function isAbsolute(string $url): bool + { + return (bool) preg_match('#^[a-z][a-z0-9+.-]*:#i', $url); + } + + + public static function removeDotSegments(string $path): string + { + $prefix = $segment = ''; + if (str_starts_with($path, '/')) { + $prefix = '/'; + $path = substr($path, 1); + } + $segments = explode('/', $path); + $res = []; + foreach ($segments as $segment) { + if ($segment === '..') { + array_pop($res); + } elseif ($segment !== '.') { + $res[] = $segment; + } + } + + if ($segment === '.' || $segment === '..') { + $res[] = ''; + } + return $prefix . implode('/', $res); + } } diff --git a/src/Http/UrlImmutable.php b/src/Http/UrlImmutable.php index ea9e85b8..d3d58d0f 100644 --- a/src/Http/UrlImmutable.php +++ b/src/Http/UrlImmutable.php @@ -50,7 +50,7 @@ class UrlImmutable implements \JsonSerializable private string $path = ''; private array $query = []; private string $fragment = ''; - private string $authority = ''; + private ?string $authority = null; /** @@ -60,7 +60,6 @@ public function __construct(string|self|Url $url) { $url = is_string($url) ? new Url($url) : $url; [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export(); - $this->build(); } @@ -68,7 +67,7 @@ public function withScheme(string $scheme): static { $dolly = clone $this; $dolly->scheme = $scheme; - $dolly->build(); + $dolly->authority = null; return $dolly; } @@ -79,41 +78,51 @@ public function getScheme(): string } + /** @deprecated */ public function withUser(string $user): static { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); $dolly = clone $this; $dolly->user = $user; - $dolly->build(); + $dolly->authority = null; return $dolly; } + /** @deprecated */ public function getUser(): string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->user; } + /** @deprecated */ public function withPassword(string $password): static { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); $dolly = clone $this; $dolly->password = $password; - $dolly->build(); + $dolly->authority = null; return $dolly; } + /** @deprecated */ public function getPassword(): string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->password; } + /** @deprecated */ public function withoutUserInfo(): static { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); $dolly = clone $this; $dolly->user = $dolly->password = ''; - $dolly->build(); + $dolly->authority = null; return $dolly; } @@ -122,8 +131,8 @@ public function withHost(string $host): static { $dolly = clone $this; $dolly->host = $host; - $dolly->build(); - return $dolly; + $dolly->authority = null; + return $dolly->setPath($dolly->path); } @@ -135,7 +144,7 @@ public function getHost(): string public function getDomain(int $level = 2): string { - $parts = ip2long($this->host) + $parts = filter_var($this->host, FILTER_VALIDATE_IP) ? [$this->host] : explode('.', $this->host); $parts = $level >= 0 @@ -149,7 +158,7 @@ public function withPort(int $port): static { $dolly = clone $this; $dolly->port = $port; - $dolly->build(); + $dolly->authority = null; return $dolly; } @@ -168,10 +177,14 @@ public function getDefaultPort(): ?int public function withPath(string $path): static { - $dolly = clone $this; - $dolly->path = $path; - $dolly->build(); - return $dolly; + return (clone $this)->setPath($path); + } + + + private function setPath(string $path): static + { + $this->path = $this->host && !str_starts_with($path, '/') ? '/' . $path : $path; + return $this; } @@ -185,7 +198,6 @@ public function withQuery(string|array $query): static { $dolly = clone $this; $dolly->query = is_array($query) ? $query : Url::parseQuery($query); - $dolly->build(); return $dolly; } @@ -220,7 +232,6 @@ public function withFragment(string $fragment): static { $dolly = clone $this; $dolly->fragment = $fragment; - $dolly->build(); return $dolly; } @@ -247,7 +258,15 @@ public function getAbsoluteUrl(): string */ public function getAuthority(): string { - return $this->authority; + return $this->authority ??= $this->host === '' + ? '' + : ($this->user !== '' + ? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@' + : '') + . $this->host + . ($this->port && $this->port !== $this->getDefaultPort() + ? ':' . $this->port + : ''); } @@ -256,8 +275,8 @@ public function getAuthority(): string */ public function getHostUrl(): string { - return ($this->scheme ? $this->scheme . ':' : '') - . ($this->authority !== '' ? '//' . $this->authority : ''); + return ($this->scheme === '' ? '' : $this->scheme . ':') + . ($this->host === '' ? '' : '//' . $this->getAuthority()); } @@ -273,33 +292,57 @@ public function isEqual(string|Url|self $url): bool } - public function jsonSerialize(): string + /** + * Resolves relative URLs in the same way as browser. If path is relative, it is resolved against + * base URL, if begins with /, it is resolved against the host root. + */ + public function resolve(string $reference): self { - return $this->getAbsoluteUrl(); + $ref = new self($reference); + if ($ref->scheme !== '') { + $ref->path = Url::removeDotSegments($ref->path); + return $ref; + } + + $ref->scheme = $this->scheme; + + if ($ref->host !== '') { + $ref->path = Url::removeDotSegments($ref->path); + return $ref; + } + + $ref->host = $this->host; + $ref->port = $this->port; + + if ($ref->path === '') { + $ref->path = $this->path; + $ref->query = $ref->query ?: $this->query; + } elseif (str_starts_with($ref->path, '/')) { + $ref->path = Url::removeDotSegments($ref->path); + } else { + $ref->path = Url::removeDotSegments($this->mergePath($ref->path)); + } + return $ref; } /** @internal */ - final public function export(): array + protected function mergePath(string $path): string { - return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment]; + $pos = strrpos($this->path, '/'); + return $pos === false ? $path : substr($this->path, 0, $pos + 1) . $path; } - protected function build(): void + public function jsonSerialize(): string { - if ($this->host && !str_starts_with($this->path, '/')) { - $this->path = '/' . $this->path; - } + return $this->getAbsoluteUrl(); + } - $this->authority = $this->host === '' - ? '' - : ($this->user !== '' - ? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@' - : '') - . $this->host - . ($this->port && $this->port !== $this->getDefaultPort() - ? ':' . $this->port - : ''); + + /** @internal */ + final public function export(): array + { + return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment]; } } diff --git a/src/Http/UrlScript.php b/src/Http/UrlScript.php index 9de766d6..b3886508 100644 --- a/src/Http/UrlScript.php +++ b/src/Http/UrlScript.php @@ -40,18 +40,30 @@ class UrlScript extends UrlImmutable public function __construct(string|Url $url = '/', string $scriptPath = '') { - $this->scriptPath = $scriptPath; parent::__construct($url); - $this->build(); + $this->setScriptPath($scriptPath); } public function withPath(string $path, string $scriptPath = ''): static { - $dolly = clone $this; - $dolly->scriptPath = $scriptPath; - $parent = UrlImmutable::withPath(...)->bindTo($dolly); - return $parent($path); + $dolly = parent::withPath($path); + $dolly->setScriptPath($scriptPath); + return $dolly; + } + + + private function setScriptPath(string $scriptPath): void + { + $path = $this->getPath(); + $scriptPath = $scriptPath ?: $path; + $pos = strrpos($scriptPath, '/'); + if ($pos === false || strncmp($scriptPath, $path, $pos + 1)) { + throw new Nette\InvalidArgumentException("ScriptPath '$scriptPath' doesn't match path '$path'"); + } + + $this->scriptPath = $scriptPath; + $this->basePath = substr($scriptPath, 0, $pos + 1); } @@ -94,16 +106,9 @@ public function getPathInfo(): string } - protected function build(): void + /** @internal */ + protected function mergePath(string $path): string { - parent::build(); - $path = $this->getPath(); - $this->scriptPath = $this->scriptPath ?: $path; - $pos = strrpos($this->scriptPath, '/'); - if ($pos === false || strncmp($this->scriptPath, $path, $pos + 1)) { - throw new Nette\InvalidArgumentException("ScriptPath '$this->scriptPath' doesn't match path '$path'"); - } - - $this->basePath = substr($this->scriptPath, 0, $pos + 1); + return $this->basePath . $path; } } diff --git a/src/Http/UserStorage.php b/src/Http/UserStorage.php deleted file mode 100644 index bbcebfb8..00000000 --- a/src/Http/UserStorage.php +++ /dev/null @@ -1,179 +0,0 @@ -getSessionSection(true); - $section->authenticated = $state; - - // Session Fixation defence - $this->sessionHandler->regenerateId(); - - if ($state) { - $section->reason = null; - $section->authTime = time(); // informative value - - } else { - $section->reason = self::MANUAL; - $section->authTime = null; - } - - return $this; - } - - - /** - * Is this user authenticated? - */ - public function isAuthenticated(): bool - { - $session = $this->getSessionSection(false); - return $session && $session->authenticated; - } - - - /** - * Sets the user identity. - */ - public function setIdentity(?IIdentity $identity): self - { - $this->getSessionSection(true)->identity = $identity; - return $this; - } - - - /** - * Returns current user identity, if any. - */ - public function getIdentity(): ?Nette\Security\IIdentity - { - $session = $this->getSessionSection(false); - return $session ? $session->identity : null; - } - - - /** - * Changes namespace; allows more users to share a session. - */ - public function setNamespace(string $namespace): self - { - if ($this->namespace !== $namespace) { - $this->namespace = $namespace; - $this->sessionSection = null; - } - - return $this; - } - - - /** - * Returns current namespace. - */ - public function getNamespace(): string - { - return $this->namespace; - } - - - /** - * Enables log out after inactivity. Accepts flag IUserStorage::CLEAR_IDENTITY. - */ - public function setExpiration(?string $time, int $flags = 0): self - { - $section = $this->getSessionSection(true); - if ($time) { - $time = Nette\Utils\DateTime::from($time)->format('U'); - $section->expireTime = $time; - $section->expireDelta = $time - time(); - - } else { - unset($section->expireTime, $section->expireDelta); - } - - $section->expireIdentity = (bool) ($flags & self::CLEAR_IDENTITY); - $section->setExpiration($time, 'foo'); // time check - return $this; - } - - - /** - * Why was user logged out? - */ - public function getLogoutReason(): ?int - { - $session = $this->getSessionSection(false); - return $session ? $session->reason : null; - } - - - /** - * Returns and initializes $this->sessionSection. - */ - protected function getSessionSection(bool $need): ?SessionSection - { - if ($this->sessionSection !== null) { - return $this->sessionSection; - } - - if (!$need && !$this->sessionHandler->exists()) { - return null; - } - - $this->sessionSection = $section = $this->sessionHandler->getSection('Nette.Http.UserStorage/' . $this->namespace); - - if (!$section->identity instanceof IIdentity || !is_bool($section->authenticated)) { - $section->remove(); - } - - if ($section->authenticated && $section->expireDelta > 0) { // check time expiration - if ($section->expireTime < time()) { - $section->reason = self::INACTIVITY; - $section->authenticated = false; - if ($section->expireIdentity) { - unset($section->identity); - } - } - - $section->expireTime = time() + $section->expireDelta; // sliding expiration - } - - if (!$section->authenticated) { - unset($section->expireTime, $section->expireDelta, $section->expireIdentity, $section->authTime); - } - - return $this->sessionSection; - } -} diff --git a/tests/Http/FileUpload.basic.phpt b/tests/Http/FileUpload.basic.phpt index fc01ab61..d39f6f15 100644 --- a/tests/Http/FileUpload.basic.phpt +++ b/tests/Http/FileUpload.basic.phpt @@ -22,7 +22,7 @@ test('', function () { 'size' => 209, ]); - Assert::same('readme.txt', $upload->getName()); + Assert::same('readme.txt', @$upload->getName()); // deprecated Assert::same('readme.txt', $upload->getUntrustedName()); Assert::same('readme.txt', $upload->getSanitizedName()); Assert::same('path/to/readme.txt', $upload->getUntrustedFullPath()); @@ -47,7 +47,7 @@ test('', function () { 'size' => 209, ]); - Assert::same('../.image.png', $upload->getName()); + Assert::same('../.image.png', $upload->getUntrustedName()); Assert::same('image.png', $upload->getSanitizedName()); Assert::same('../.image.png', $upload->getUntrustedFullPath()); Assert::same('image/png', $upload->getContentType()); @@ -85,3 +85,22 @@ test('', function () { Assert::null($upload->getSuggestedExtension()); Assert::same('', (string) $upload); }); + + +test('', function () { + $upload = new FileUpload($file = __DIR__ . '/files/file.txt'); + + Assert::same('file.txt', $upload->getName()); + Assert::same('file.txt', $upload->getUntrustedName()); + Assert::same('file.txt', $upload->getSanitizedName()); + Assert::same($file, $upload->getUntrustedFullPath()); + Assert::same(filesize($file), $upload->getSize()); + Assert::same($file, $upload->getTemporaryFile()); + Assert::same($file, (string) $upload); + Assert::same(0, $upload->getError()); + Assert::true($upload->isOk()); + Assert::true($upload->hasFile()); + Assert::false($upload->isImage()); + Assert::null($upload->getSuggestedExtension()); + Assert::same(file_get_contents($file), $upload->getContents()); +}); diff --git a/tests/Http/Request.invalidEncoding.phpt b/tests/Http/Request.invalidEncoding.phpt index 96410a3f..30fb80c1 100644 --- a/tests/Http/Request.invalidEncoding.phpt +++ b/tests/Http/Request.invalidEncoding.phpt @@ -118,5 +118,5 @@ test('filtered data', function () { Assert::null($request->getFile(INVALID)); Assert::null($request->getFile(CONTROL_CHARACTERS)); Assert::type(Nette\Http\FileUpload::class, $request->files['file1']); - Assert::same('', $request->files['file1']->name); + Assert::same('', $request->files['file1']->getUntrustedName()); }); diff --git a/tests/Http/Request.request.phpt b/tests/Http/Request.request.phpt index d51e7e07..41423ce0 100644 --- a/tests/Http/Request.request.phpt +++ b/tests/Http/Request.request.phpt @@ -35,8 +35,8 @@ test('', function () { Assert::same('/file.php', $request->getUrl()->scriptPath); Assert::same('https', $request->getUrl()->scheme); - Assert::same('', $request->getUrl()->user); - Assert::same('', $request->getUrl()->password); + Assert::same('', @$request->getUrl()->user); // deprecated + Assert::same('', @$request->getUrl()->password); // deprecated Assert::same('nette.org', $request->getUrl()->host); Assert::same(8080, $request->getUrl()->port); Assert::same('/file.php', $request->getUrl()->path); @@ -61,8 +61,8 @@ test('', function () { $request = $factory->fromGlobals(); Assert::same('https', $request->getUrl()->scheme); - Assert::same('', $request->getUrl()->user); - Assert::same('', $request->getUrl()->password); + Assert::same('', @$request->getUrl()->user); // deprecated + Assert::same('', @$request->getUrl()->password); // deprecated Assert::same('nette.org', $request->getUrl()->host); Assert::same(8080, $request->getUrl()->port); Assert::same('/file.php', $request->getUrl()->path); diff --git a/tests/Http/RequestFactory.authorization.phpt b/tests/Http/RequestFactory.authorization.phpt index 7ddbb6d4..cd73e1fe 100644 --- a/tests/Http/RequestFactory.authorization.phpt +++ b/tests/Http/RequestFactory.authorization.phpt @@ -26,8 +26,8 @@ test('Basic', function () { ); Assert::same(['user', 'password'], $request->getBasicCredentials()); - Assert::same('', $request->getUrl()->getUser()); - Assert::same('', $request->getUrl()->getPassword()); + Assert::same('', @$request->getUrl()->getUser()); // deprecated + Assert::same('', @$request->getUrl()->getPassword()); // deprecated }); diff --git a/tests/Http/RequestFactory.proxy.forwarded.phpt b/tests/Http/RequestFactory.proxy.forwarded.phpt index 3805db12..37083f7a 100644 --- a/tests/Http/RequestFactory.proxy.forwarded.phpt +++ b/tests/Http/RequestFactory.proxy.forwarded.phpt @@ -25,7 +25,7 @@ test('', function () { $factory->setProxy('127.0.0.1/8'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('http', $url->getScheme()); @@ -43,7 +43,7 @@ test('', function () { $factory->setProxy('127.0.0.3'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same(8080, $url->getPort()); @@ -62,7 +62,7 @@ test('', function () { $factory->setProxy('127.0.0.3'); Assert::same('2001:db8:cafe::17', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('2001:db8:cafe::17', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('[2001:db8:cafe::18]', $url->getHost()); @@ -79,7 +79,7 @@ test('', function () { $factory->setProxy('127.0.0.3'); Assert::same('2001:db8:cafe::17', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('2001:db8:cafe::17', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same(47832, $url->getPort()); diff --git a/tests/Http/RequestFactory.proxy.x-forwarded.phpt b/tests/Http/RequestFactory.proxy.x-forwarded.phpt index 7b3fa87b..9d6a3e03 100644 --- a/tests/Http/RequestFactory.proxy.x-forwarded.phpt +++ b/tests/Http/RequestFactory.proxy.x-forwarded.phpt @@ -27,7 +27,7 @@ test('', function () { $factory->setProxy('127.0.0.1/8'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('otherhost', $url->getHost()); @@ -45,7 +45,7 @@ test('', function () { $factory = new RequestFactory; $factory->setProxy('127.0.0.3'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('otherhost', $url->getHost()); @@ -81,7 +81,7 @@ test('', function () { $factory = new RequestFactory; $factory->setProxy('10.0.0.0/24'); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); @@ -89,7 +89,7 @@ test('', function () { $factory->setProxy(['10.0.0.1', '10.0.0.2']); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); @@ -107,7 +107,7 @@ test('', function () { $factory = new RequestFactory; $factory->setProxy(['10.0.0.1', '10.0.0.2']); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); + Assert::null($factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); diff --git a/tests/Http/Url.canonicalize.phpt b/tests/Http/Url.canonicalize.phpt index d8412a8a..68424497 100644 --- a/tests/Http/Url.canonicalize.phpt +++ b/tests/Http/Url.canonicalize.phpt @@ -26,7 +26,7 @@ $url = new Url('http://host/%1f%20 %21!%22"%23%24$%25%26&%27\'%28(%29)%2a*%2b+%2 . '%41A%42B%43C%44D%45E%46F%47G%48H%49I%4aJ%4bK%4cL%4dM%4eN%4fO%50P%51Q%52R%53S%54T%55U%56V%57W%58X%59Y%5aZ%5b[%5c\%5d]%5e^%5f_%60`' . '%61a%62b%63c%64d%65e%66f%67g%68h%69i%6aj%6bk%6cl%6dm%6en%6fo%70p%71q%72r%73s%74t%75u%76v%77w%78x%79y%7az%7b{%7c|%7d}%7e~%7fá'); $url->canonicalize(); -Assert::same('http://host/%1F%20%20!!%22%22%23$$%25&&\'\'(())**++,,--..%2F/00112233445566778899::;;%3C%3C==%3E%3E%3F@@' +Assert::same('http://host/%1F%20%20!!""%23$$%25&&\'\'(())**++,,--..%2F/00112233445566778899::;;%3C%3C==%3E%3E%3F@@' . 'AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ%5B%5B%5C%5C%5D%5D%5E%5E__%60%60' . 'aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz%7B%7B%7C%7C%7D%7D~~%7F%C3%A1', (string) $url); diff --git a/tests/Http/Url.fileScheme.phpt b/tests/Http/Url.fileScheme.phpt index 518d247b..070d9256 100644 --- a/tests/Http/Url.fileScheme.phpt +++ b/tests/Http/Url.fileScheme.phpt @@ -16,8 +16,8 @@ test('', function () { $url = new Url('file://localhost/D:/dokumentace/rfc3986.txt'); Assert::same('file://localhost/D:/dokumentace/rfc3986.txt', (string) $url); Assert::same('file', $url->scheme); - Assert::same('', $url->user); - Assert::same('', $url->password); + Assert::same('', @$url->user); // deprecated + Assert::same('', @$url->password); // deprecated Assert::same('localhost', $url->host); Assert::null($url->port); Assert::same('/D:/dokumentace/rfc3986.txt', $url->path); @@ -30,8 +30,8 @@ test('', function () { $url = new Url('file:///D:/dokumentace/rfc3986.txt'); Assert::same('file:D:/dokumentace/rfc3986.txt', (string) $url); Assert::same('file', $url->scheme); - Assert::same('', $url->user); - Assert::same('', $url->password); + Assert::same('', @$url->user); // deprecated + Assert::same('', @$url->password); // deprecated Assert::same('', $url->host); Assert::null($url->port); Assert::same('D:/dokumentace/rfc3986.txt', $url->path); diff --git a/tests/Http/Url.ftpScheme.phpt b/tests/Http/Url.ftpScheme.phpt index 68ed1af8..f9ef2932 100644 --- a/tests/Http/Url.ftpScheme.phpt +++ b/tests/Http/Url.ftpScheme.phpt @@ -15,8 +15,8 @@ require __DIR__ . '/../bootstrap.php'; $url = new Url('ftp://ftp.is.co.za/rfc/rfc3986.txt'); Assert::same('ftp', $url->scheme); -Assert::same('', $url->user); -Assert::same('', $url->password); +Assert::same('', @$url->user); // deprecated +Assert::same('', @$url->password); // deprecated Assert::same('ftp.is.co.za', $url->host); Assert::same(21, $url->port); Assert::same('/rfc/rfc3986.txt', $url->path); diff --git a/tests/Http/Url.httpScheme.phpt b/tests/Http/Url.httpScheme.phpt index 1ee32b68..b376f32f 100644 --- a/tests/Http/Url.httpScheme.phpt +++ b/tests/Http/Url.httpScheme.phpt @@ -17,8 +17,8 @@ $url = new Url('http://username%3A:password%3A@hostn%61me:60/p%61th/script.php?% Assert::same('http://username%3A:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', (string) $url); Assert::same('"http:\/\/username%3A:password%3A@hostname:60\/p%61th\/script.php?arg=value#anchor"', json_encode($url)); Assert::same('http', $url->scheme); -Assert::same('username:', $url->user); -Assert::same('password:', $url->password); +Assert::same('username:', @$url->user); // deprecated +Assert::same('password:', @$url->password); // deprecated Assert::same('hostname', $url->host); Assert::same(60, $url->port); Assert::same(80, $url->getDefaultPort()); diff --git a/tests/Http/Url.isAbsolute.phpt b/tests/Http/Url.isAbsolute.phpt new file mode 100644 index 00000000..fbdb409e --- /dev/null +++ b/tests/Http/Url.isAbsolute.phpt @@ -0,0 +1,15 @@ +scheme); -Assert::same('username:', $url->user); -Assert::same('password:', $url->password); +Assert::same('username:', @$url->user); // deprecated +Assert::same('password:', @$url->password); // deprecated Assert::same('hostname', $url->host); Assert::same(60, $url->port); Assert::same('hostname', $url->getDomain()); diff --git a/tests/Http/UrlImmutable.manipulation.phpt b/tests/Http/UrlImmutable.manipulation.phpt index 06d66c41..f2075138 100644 --- a/tests/Http/UrlImmutable.manipulation.phpt +++ b/tests/Http/UrlImmutable.manipulation.phpt @@ -14,10 +14,10 @@ test('', function () { $url = $url->withScheme(''); Assert::same('//username%3A:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', $url->absoluteUrl); - $url = $url->withUser('name'); + $url = @$url->withUser('name'); // deprecated Assert::same('//name:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', $url->absoluteUrl); - $url = $url->withPassword('secret'); + $url = @$url->withPassword('secret'); // deprecated Assert::same('//name:secret@hostname:60/p%61th/script.php?arg=value#anchor', $url->absoluteUrl); $url = $url->withHost('localhost'); @@ -29,7 +29,7 @@ test('', function () { $url = $url->withFragment('hello'); Assert::same('//name:secret@localhost:123/p%61th/script.php?arg=value#hello', $url->absoluteUrl); - $url = $url->withoutUserInfo(); + $url = @$url->withoutUserInfo(); // deprecated Assert::same('//localhost:123/p%61th/script.php?arg=value#hello', $url->absoluteUrl); }); diff --git a/tests/Http/UrlImmutable.resolve.phpt b/tests/Http/UrlImmutable.resolve.phpt new file mode 100644 index 00000000..bae37593 --- /dev/null +++ b/tests/Http/UrlImmutable.resolve.phpt @@ -0,0 +1,69 @@ + [ + // absolute URLs with various schemes + 'a:' => 'a:', + 'a:b' => 'a:b', + 'http://other.com/test' => 'http://other.com/test', + 'https://other.com/test' => 'https://other.com/test', + 'ftp://other.com/test' => 'ftp://other.com/test', + + // protocol-relative URLs - keep current scheme + '//other.com/test' => 'https://other.com/test', + + // root-relative paths + '/test' => 'https://example.com/test', + '/test/' => 'https://example.com/test/', + + // relative paths + 'sibling' => 'https://example.com/path/sibling', + '../parent' => 'https://example.com/parent', + 'child/' => 'https://example.com/path/child/', + ], + + // base dir with query string and fragment + 'https://example.com/path/?q=123#frag' => [ + '' => 'https://example.com/path/?q=123', + 'file' => 'https://example.com/path/file', + './file' => 'https://example.com/path/file', + '/root' => 'https://example.com/root', + 'subdir/?q=456' => 'https://example.com/path/subdir/?q=456', + 'subdir/#frag' => 'https://example.com/path/subdir/#frag', + '../file' => 'https://example.com/file', + '/../file' => 'https://example.com/file', + 'file?newq=/..#newfrag/..' => 'https://example.com/path/file?newq=%2F..#newfrag/..', + '?newq=/..' => 'https://example.com/path/?newq=%2F..', + '#newfrag/..' => 'https://example.com/path/?q=123#newfrag/..', + ], + + // base file with query string and fragment + 'https://example.com/path/file?q=123#frag' => [ + '' => 'https://example.com/path/file?q=123', + 'file' => 'https://example.com/path/file', + './file' => 'https://example.com/path/file', + '/root' => 'https://example.com/root', + 'subdir/file?q=123' => 'https://example.com/path/subdir/file?q=123', + 'subdir/file#frag' => 'https://example.com/path/subdir/file#frag', + '../file' => 'https://example.com/file', + '/../file' => 'https://example.com/file', + '?newq=/..' => 'https://example.com/path/file?newq=%2F..', + '#newfrag/..' => 'https://example.com/path/file?q=123#newfrag/..', + ], +]; + + +foreach ($tests as $base => $paths) { + $url = new UrlImmutable($base); + foreach ($paths as $path => $expected) { + Assert::same($expected, (string) $url->resolve($path), "Base: $base, Reference: $path"); + } +} diff --git a/tests/Http/UrlImmutable.usage.phpt b/tests/Http/UrlImmutable.usage.phpt index fcd1017a..d675dd3f 100644 --- a/tests/Http/UrlImmutable.usage.phpt +++ b/tests/Http/UrlImmutable.usage.phpt @@ -17,8 +17,8 @@ $url = new UrlImmutable('http://username%3A:password%3A@hostn%61me:60/p%61th/scr Assert::same('http://username%3A:password%3A@hostname:60/p%61th/script.php?arg=value#anchor', (string) $url); Assert::same('"http:\/\/username%3A:password%3A@hostname:60\/p%61th\/script.php?arg=value#anchor"', json_encode($url)); Assert::same('http', $url->scheme); -Assert::same('username:', $url->user); -Assert::same('password:', $url->password); +Assert::same('username:', @$url->user); // deprecated +Assert::same('password:', @$url->password); // deprecated Assert::same('hostname', $url->host); Assert::same(60, $url->port); Assert::same('hostname', $url->getDomain()); diff --git a/tests/Http/UrlScript.resolve.phpt b/tests/Http/UrlScript.resolve.phpt new file mode 100644 index 00000000..7e4222db --- /dev/null +++ b/tests/Http/UrlScript.resolve.phpt @@ -0,0 +1,69 @@ + [ + // absolute URLs with various schemes + 'a:' => 'a:', + 'a:b' => 'a:b', + 'http://other.com/test' => 'http://other.com/test', + 'https://other.com/test' => 'https://other.com/test', + 'ftp://other.com/test' => 'ftp://other.com/test', + + // protocol-relative URLs - keep current scheme + '//other.com/test' => 'https://other.com/test', + + // root-relative paths + '/test' => 'https://example.com/test', + '/test/' => 'https://example.com/test/', + + // relative paths + 'sibling' => 'https://example.com/sibling', + '../parent' => 'https://example.com/parent', + 'child/' => 'https://example.com/child/', + ], + + // base dir with query string and fragment + 'https://example.com/path/?q=123#frag' => [ + '' => 'https://example.com/path/?q=123', + 'file' => 'https://example.com/file', + './file' => 'https://example.com/file', + '/root' => 'https://example.com/root', + 'subdir/?q=456' => 'https://example.com/subdir/?q=456', + 'subdir/#frag' => 'https://example.com/subdir/#frag', + '../file' => 'https://example.com/file', + '/../file' => 'https://example.com/file', + 'file?newq=/..#newfrag/..' => 'https://example.com/file?newq=%2F..#newfrag/..', + '?newq=/..' => 'https://example.com/path/?newq=%2F..', + '#newfrag/..' => 'https://example.com/path/?q=123#newfrag/..', + ], + + // base file with query string and fragment + 'https://example.com/path/file?q=123#frag' => [ + '' => 'https://example.com/path/file?q=123', + 'file' => 'https://example.com/file', + './file' => 'https://example.com/file', + '/root' => 'https://example.com/root', + 'subdir/file?q=123' => 'https://example.com/subdir/file?q=123', + 'subdir/file#frag' => 'https://example.com/subdir/file#frag', + '../file' => 'https://example.com/file', + '/../file' => 'https://example.com/file', + '?newq=/..' => 'https://example.com/path/file?newq=%2F..', + '#newfrag/..' => 'https://example.com/path/file?q=123#newfrag/..', + ], +]; + + +foreach ($tests as $base => $paths) { + $url = new UrlScript($base, '/'); + foreach ($paths as $path => $expected) { + Assert::same($expected, (string) $url->resolve($path), "Base: $base, Reference: $path"); + } +}