From 9d1b66d19e30bc5b258b2c3a8978bd5f17f1dbc0 Mon Sep 17 00:00:00 2001 From: Alex Cabal Date: Tue, 25 Feb 2025 22:09:35 -0600 Subject: [PATCH] Update PHPStan and Safe PHP, and review codebase for further type correctness --- composer.json | 6 +- composer.lock | 173 ++++++++++++------ config/phpstan/phpstan.neon | 1 - lib/Artwork.php | 15 +- lib/AtomFeed.php | 6 +- lib/Constants.php | 1 + lib/Core.php | 2 + lib/Ebook.php | 26 +-- lib/Email.php | 6 +- lib/HttpInput.php | 80 ++++++-- lib/Image.php | 8 +- lib/Manual.php | 4 +- lib/Museum.php | 7 +- lib/Poll.php | 4 +- lib/PollItem.php | 2 +- lib/Template.php | 5 +- scripts/ingest-fa-payments | 1 + scripts/process-pending-payments | 1 + scripts/update-project-statuses | 3 +- templates/ArtworkStatus.php | 25 +-- templates/DonationAlert.php | 16 +- templates/Header.php | 12 +- templates/OpdsNavigationFeed.php | 2 +- www/artists/get.php | 4 +- www/artworks/index.php | 5 +- www/bulk-downloads/download.php | 4 +- ...ate-svgs-from-maps-with-several-colors.php | 4 +- www/ebooks/index.php | 4 +- www/feeds/401.php | 4 +- www/manual/index.php | 1 - www/polls/get.php | 4 +- www/settings/post.php | 1 + www/webhooks/github.php | 13 +- www/webhooks/postmark.php | 11 +- www/webhooks/zoho.php | 9 +- 35 files changed, 301 insertions(+), 169 deletions(-) diff --git a/composer.json b/composer.json index e233a1c3..9873b08b 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,11 @@ ] }, "require-dev": { - "phpstan/phpstan": "^1.11.1", - "thecodingmachine/phpstan-safe-rule": "^1.2.0" + "phpstan/phpstan": "^2.1.6", + "thecodingmachine/phpstan-safe-rule": "^1.4.0" }, "require": { - "thecodingmachine/safe": "^2.5.0", + "thecodingmachine/safe": "^3.0.2", "phpmailer/phpmailer": "^6.6.0", "ramsey/uuid": "^4.7.6", "gregwar/captcha": "^1.2.0", diff --git a/composer.lock b/composer.lock index 466fec5b..4b904866 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b0f9117ed0fa1c3563107eca1d2a77b9", + "content-hash": "958c9c0a2d1f5242e7c4952657999fe4", "packages": [ { "name": "brick/math", @@ -550,16 +550,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.13", + "version": "v6.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958" + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/daea9eca0b08d0ed1dc9ab702a46128fd1be4958", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { @@ -594,7 +594,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.13" + "source": "https://github.com/symfony/finder/tree/v6.4.17" }, "funding": [ { @@ -610,7 +610,7 @@ "type": "tidelift" } ], - "time": "2024-10-01T08:30:56+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -638,8 +638,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -755,46 +755,31 @@ }, { "name": "thecodingmachine/safe", - "version": "v2.5.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + "reference": "22ffad3248982a784f9870a37aeb2e522bd19645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", - "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/22ffad3248982a784f9870a37aeb2e522bd19645", + "reference": "22ffad3248982a784f9870a37aeb2e522bd19645", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { - "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^1.0" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, "autoload": { "files": [ - "deprecated/apc.php", - "deprecated/array.php", - "deprecated/datetime.php", - "deprecated/libevent.php", - "deprecated/misc.php", - "deprecated/password.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "deprecated/strings.php", "lib/special_cases.php", - "deprecated/mysqli.php", "generated/apache.php", "generated/apcu.php", "generated/array.php", @@ -834,6 +819,7 @@ "generated/mbstring.php", "generated/misc.php", "generated/mysql.php", + "generated/mysqli.php", "generated/network.php", "generated/oci8.php", "generated/opcache.php", @@ -846,6 +832,7 @@ "generated/ps.php", "generated/pspell.php", "generated/readline.php", + "generated/rnp.php", "generated/rpminfo.php", "generated/rrd.php", "generated/sem.php", @@ -877,7 +864,6 @@ "lib/DateTime.php", "lib/DateTimeImmutable.php", "lib/Exceptions/", - "deprecated/Exceptions/", "generated/Exceptions/" ] }, @@ -888,28 +874,100 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" + "source": "https://github.com/thecodingmachine/safe/tree/v3.0.2" }, - "time": "2023-04-05T11:54:14+00:00" + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-02-19T19:23:00+00:00" } ], "packages-dev": [ { - "name": "phpstan/phpstan", - "version": "1.12.11", + "name": "nikic/php-parser", + "version": "v5.4.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -950,41 +1008,42 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2025-02-19T15:46:42+00:00" }, { "name": "thecodingmachine/phpstan-safe-rule", - "version": "v1.2.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", - "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e" + "reference": "33dcbc3228c55ea4c364ecf74a3661cf7b7f168d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/8a7b88e0d54f209a488095085f183e9174c40e1e", - "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e", + "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/33dcbc3228c55ea4c364ecf74a3661cf7b7f168d", + "reference": "33dcbc3228c55ea4c364ecf74a3661cf7b7f168d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0", - "thecodingmachine/safe": "^1.0 || ^2.0" + "nikic/php-parser": "^5", + "php": "^8.1", + "phpstan/phpstan": "^2.0", + "thecodingmachine/safe": "^1.2 || ^2.0 || ^3.0" }, "require-dev": { "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^7.5.2 || ^8.0", + "phpunit/phpunit": "^10.4", "squizlabs/php_codesniffer": "^3.4" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - }, "phpstan": { "includes": [ "phpstan-safe-rule.neon" ] + }, + "branch-alias": { + "dev-master": "2.0-dev" } }, "autoload": { @@ -1005,9 +1064,9 @@ "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", "support": { "issues": "https://github.com/thecodingmachine/phpstan-safe-rule/issues", - "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.2.0" + "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.4.0" }, - "time": "2022-01-17T10:12:29+00:00" + "time": "2025-02-11T12:41:29+00:00" } ], "aliases": [], diff --git a/config/phpstan/phpstan.neon b/config/phpstan/phpstan.neon index e21d70f3..a780a522 100644 --- a/config/phpstan/phpstan.neon +++ b/config/phpstan/phpstan.neon @@ -40,7 +40,6 @@ parameters: -'#^Safe\\#' uncheckedExceptionClasses: - 'Exceptions\DatabaseQueryException' - - 'Exceptions\DuplicateDatabaseKeyException' - 'Exceptions\MultiSelectMethodNotFoundException' - 'PDOException' - 'TypeError' diff --git a/lib/Artwork.php b/lib/Artwork.php index 9e49fd0f..7ec2752f 100644 --- a/lib/Artwork.php +++ b/lib/Artwork.php @@ -254,7 +254,7 @@ class Artwork{ if(!isset($this->Dimensions)){ $this->_Dimensions = ''; try{ - list($imageWidth, $imageHeight) = getimagesize($this->ImageFsPath); + list($imageWidth, $imageHeight) = (getimagesize($this->ImageFsPath) ?? throw new \Exception()); if($imageWidth && $imageHeight){ $this->_Dimensions = number_format($imageWidth) . ' × ' . number_format($imageHeight); } @@ -529,9 +529,14 @@ class Artwork{ } // Check for minimum dimensions. - list($imageWidth, $imageHeight) = getimagesize($imagePath); - if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){ - $error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException()); + try{ + list($imageWidth, $imageHeight) = (getimagesize($imagePath) ?? throw new \Exception()); + if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){ + $error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException()); + } + } + catch(\Exception){ + $error->Add(new Exceptions\InvalidImageUploadException()); } } } @@ -647,7 +652,7 @@ class Artwork{ } preg_match('|^/books/edition/[^/]+/([^/]+)$|ius', $parsedUrl['path'], $matches); - $id = $matches[1]; + $id = $matches[1] ?? ''; parse_str($parsedUrl['query'] ?? '', $vars); diff --git a/lib/AtomFeed.php b/lib/AtomFeed.php index 32f6ba76..e7dc212b 100644 --- a/lib/AtomFeed.php +++ b/lib/AtomFeed.php @@ -61,10 +61,8 @@ class AtomFeed extends Feed{ } } else{ - if($entry->Updated !== null){ - $obj->Updated = $entry->Updated->format(Enums\DateTimeFormat::Iso->value); - $obj->Id = $entry->Id; - } + $obj->Updated = $entry->Updated->format(Enums\DateTimeFormat::Iso->value); + $obj->Id = $entry->Id; } if(isset($obj->Id)){ diff --git a/lib/Constants.php b/lib/Constants.php index 3352f5ea..47c5f9d8 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -1,6 +1,7 @@ _HeroImageUrl; } - protected function GetHeroImageAvifUrl(): ?string{ + protected function GetHeroImageAvifUrl(): string{ if(!isset($this->_HeroImageAvifUrl)){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero.avif')){ $this->_HeroImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . substr(sha1($this->Updated->format(Enums\DateTimeFormat::UnixTimestamp->value)), 0, 8) . '-hero.avif'; @@ -486,7 +486,7 @@ final class Ebook{ return $this->_HeroImage2xUrl; } - protected function GetHeroImage2xAvifUrl(): ?string{ + protected function GetHeroImage2xAvifUrl(): string{ if(!isset($this->_HeroImage2xAvifUrl)){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero@2x.avif')){ $this->_HeroImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . substr(sha1($this->Updated->format(Enums\DateTimeFormat::UnixTimestamp->value)), 0, 8) . '-hero@2x.avif'; @@ -507,7 +507,7 @@ final class Ebook{ return $this->_CoverImageUrl; } - protected function GetCoverImageAvifUrl(): ?string{ + protected function GetCoverImageAvifUrl(): string{ if(!isset($this->_CoverImageAvifUrl)){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover.avif')){ $this->_CoverImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . substr(sha1($this->Updated->format(Enums\DateTimeFormat::UnixTimestamp->value)), 0, 8) . '-cover.avif'; @@ -528,7 +528,7 @@ final class Ebook{ return $this->_CoverImage2xUrl; } - protected function GetCoverImage2xAvifUrl(): ?string{ + protected function GetCoverImage2xAvifUrl(): string{ if(!isset($this->_CoverImage2xAvifUrl)){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover@2x.avif')){ $this->_CoverImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . substr(sha1($this->Updated->format(Enums\DateTimeFormat::UnixTimestamp->value)), 0, 8) . '-cover@2x.avif'; @@ -839,7 +839,7 @@ final class Ebook{ // Fill in the short history of this repo. try{ - $historyEntries = explode("\n", shell_exec('cd ' . escapeshellarg($ebook->RepoFilesystemPath) . ' && git log -n5 --pretty=format:"%ct %H %s"')); + $historyEntries = explode("\n", shell_exec('cd ' . escapeshellarg($ebook->RepoFilesystemPath) . ' && git log -n5 --pretty=format:"%ct %H %s"') ?? ''); $gitCommits = []; foreach($historyEntries as $logLine){ @@ -873,13 +873,13 @@ final class Ebook{ $ebook->AlternateTitle = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="dcterms:alternate"][@refines="#title"]')); $date = $xml->xpath('/package/metadata/dc:date') ?: []; - if($date !== false && sizeof($date) > 0){ + if(sizeof($date) > 0){ /** @throws void */ $ebook->EbookCreated = new DateTimeImmutable((string)$date[0]); } $modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]') ?: []; - if($modifiedDate !== false && sizeof($modifiedDate) > 0){ + if(sizeof($modifiedDate) > 0){ /** @throws void */ $ebook->EbookUpdated = new DateTimeImmutable((string)$modifiedDate[0]); } @@ -949,7 +949,7 @@ final class Ebook{ $fileAs = null; $fileAsElement = $xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]') ?: []; - if($fileAsElement !== false && sizeof($fileAsElement) > 0){ + if(sizeof($fileAsElement) > 0){ $fileAs = (string)$fileAsElement[0]; } else{ @@ -1030,15 +1030,15 @@ final class Ebook{ $ebook->Language = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:language')) ?? ''; $wordCount = 0; - $wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]'); - if($wordCountElement !== false && sizeof($wordCountElement) > 0){ + $wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]') ?: []; + if(sizeof($wordCountElement) > 0){ $wordCount = (int)$wordCountElement[0]; } $ebook->WordCount = $wordCount; $readingEase = 0; - $readingEaseElement = $xml->xpath('/package/metadata/meta[@property="se:reading-ease.flesch"]'); - if($readingEaseElement !== false && sizeof($readingEaseElement) > 0){ + $readingEaseElement = $xml->xpath('/package/metadata/meta[@property="se:reading-ease.flesch"]') ?: []; + if(sizeof($readingEaseElement) > 0){ $readingEase = (float)$readingEaseElement[0]; } $ebook->ReadingEase = $readingEase; @@ -1124,7 +1124,7 @@ final class Ebook{ */ protected static function MatchContributorUrlNameToIdentifier(string $urlName, string $identifier): string{ if(preg_match('|' . $urlName . '[^\/_]*|ius', $identifier, $matches)){ - return $matches[0]; + return $matches[0] ?? ''; } else{ return $urlName; diff --git a/lib/Email.php b/lib/Email.php index 513e6f4b..b8829431 100644 --- a/lib/Email.php +++ b/lib/Email.php @@ -45,7 +45,7 @@ class Email{ $phpMailer->AddAddress($this->To, $this->ToName); $phpMailer->Subject = $this->Subject; $phpMailer->CharSet = 'UTF-8'; - if($this->TextBody !== null && $this->TextBody != ''){ + if($this->TextBody != ''){ $phpMailer->IsHTML(true); $phpMailer->Body = $this->Body; $phpMailer->AltBody = $this->TextBody; @@ -55,9 +55,7 @@ class Email{ } foreach($this->Attachments as $attachment){ - if(is_array($attachment)){ - $phpMailer->addStringAttachment($attachment['contents'], $attachment['filename']); - } + $phpMailer->addStringAttachment($attachment['contents'], $attachment['filename']); } $phpMailer->IsSMTP(); diff --git a/lib/HttpInput.php b/lib/HttpInput.php index 1206487b..1e854ea0 100644 --- a/lib/HttpInput.php +++ b/lib/HttpInput.php @@ -1,6 +1,7 @@ value + || + $_SERVER['REQUEST_METHOD'] == Enums\HttpMethod::Patch->value + || + $_SERVER['REQUEST_METHOD'] == Enums\HttpMethod::Put->value + ) + && + preg_match('/^application\/x-www-form-urlencoded(;|$)/', $contentType) + ){ + parse_str(file_get_contents('php://input'), $_POST); + } + } + /** * Calculate the HTTP method of the request, then include `.php` and exit. */ @@ -22,10 +48,11 @@ class HttpInput{ } if($httpMethod == Enums\HttpMethod::Post){ - // If we're a HTTP POST, then we got here from a POST request initially, so just continue. + // If we're a HTTP `POST`, then we got here from a `POST` request initially, so just continue. return; } + /** @phpstan-ignore-next-line */ include($filename); exit(); @@ -47,11 +74,13 @@ class HttpInput{ * * @param ?array $allowedHttpMethods An array containing a list of allowed HTTP methods, or null if any valid HTTP method is allowed. * @param bool $throwException If the request HTTP method isn't allowed, then throw an exception; otherwise, output HTTP 405 and exit the script immediately. - * @throws Exceptions\HttpMethodNotAllowedException If the HTTP method is not allowed, and `$throwException` is `true`. + * @throws Exceptions\HttpMethodNotAllowedException If the HTTP method is recognized but not allowed, and `$throwException` is `true`. */ public static function ValidateRequestMethod(?array $allowedHttpMethods = null, bool $throwException = false): Enums\HttpMethod{ try{ - $requestMethod = Enums\HttpMethod::from($_POST['_method'] ?? $_GET['_method'] ?? $_SERVER['REQUEST_METHOD']); + /** @var string $requestMethodString */ + $requestMethodString = $_POST['_method'] ?? $_GET['_method'] ?? $_SERVER['REQUEST_METHOD']; + $requestMethod = Enums\HttpMethod::from($requestMethodString); if($allowedHttpMethods !== null){ $isRequestMethodAllowed = false; foreach($allowedHttpMethods as $allowedHttpMethod){ @@ -65,9 +94,14 @@ class HttpInput{ } } } - catch(\ValueError | Exceptions\HttpMethodNotAllowedException){ + catch(\ValueError | Exceptions\HttpMethodNotAllowedException $ex){ if($throwException){ - throw new Exceptions\HttpMethodNotAllowedException(); + if($ex instanceof \ValueError){ + throw new Exceptions\HttpMethodNotAllowedException(); + } + else{ + throw $ex; + } } else{ if($allowedHttpMethods !== null){ @@ -82,7 +116,7 @@ class HttpInput{ } /** - * @return int The maximum size for an HTTP POST request, in bytes. + * @return int The maximum size for an HTTP `POST` request, in bytes. */ public static function GetMaxPostSize(): int{ $post_max_size = ini_get('upload_max_filesize'); @@ -106,6 +140,7 @@ class HttpInput{ elseif(sizeof($_FILES) > 0){ // We received files but may have an error because the size exceeded our limit. foreach($_FILES as $file){ + /** @var array $file */ $error = $file['error'] ?? UPLOAD_ERR_OK; if($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE){ @@ -118,7 +153,9 @@ class HttpInput{ } public static function GetRequestType(): Enums\HttpRequestType{ - return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT'] ?? '') ? Enums\HttpRequestType::Web : Enums\HttpRequestType::Rest; + /** @var string $httpAccept */ + $httpAccept = $_SERVER['HTTP_ACCEPT'] ?? ''; + return preg_match('/\btext\/html\b/ius',$httpAccept) ? Enums\HttpRequestType::Web : Enums\HttpRequestType::Rest; } /** @@ -181,7 +218,7 @@ class HttpInput{ $object = $_SESSION[$variable] ?? null; - if($object !== null){ + if(is_object($object)){ foreach($class as $c){ if(is_a($object, $c)){ return $object; @@ -200,12 +237,17 @@ class HttpInput{ public static function File(string $variable): ?string{ $filePath = null; - if(isset($_FILES[$variable]) && $_FILES[$variable]['size'] > 0){ - if(!is_uploaded_file($_FILES[$variable]['tmp_name']) || $_FILES[$variable]['error'] > UPLOAD_ERR_OK){ - throw new Exceptions\InvalidFileUploadException(); - } + if(isset($_FILES[$variable])){ + /** @var array{'error': int, 'size': int, 'tmp_name': string} $file */ + $file = $_FILES[$variable]; - $filePath = $_FILES[$variable]['tmp_name'] ?? null; + if($file['size'] > 0){ + if(!is_uploaded_file($file['tmp_name']) || $file['error'] > UPLOAD_ERR_OK){ + throw new Exceptions\InvalidFileUploadException(); + } + + $filePath = $file['tmp_name']; + } } return $filePath; @@ -221,11 +263,9 @@ class HttpInput{ } /** - * @return array|array|array|array|string|int|float|bool|DateTimeImmutable|null + * @return array|array|array|array|array|string|int|float|bool|DateTimeImmutable|null */ private static function GetHttpVar(string $variable, Enums\HttpVariableType $type, Enums\HttpVariableSource $set): mixed{ - // Note that in `Core.php` we parse the request body of DELETE, PATCH, and PUT into `$_POST`. - $vars = []; switch($set){ @@ -262,6 +302,10 @@ class HttpInput{ switch($type){ case Enums\HttpVariableType::String: + if(!is_string($var)){ + return ''; + } + // Attempt to fix broken UTF8 strings, often passed by bots and scripts. // Broken UTF8 can cause exceptions in functions like `preg_replace()`. try{ @@ -282,7 +326,7 @@ class HttpInput{ } break; case Enums\HttpVariableType::Boolean: - if($var === false || $var === '0' || strtolower($var) == 'false' || strtolower($var) == 'off'){ + if($var === false || (is_string($var) && ($var === '0' || strtolower($var) == 'false' || strtolower($var) == 'off'))){ return false; } else{ @@ -299,7 +343,7 @@ class HttpInput{ } break; case Enums\HttpVariableType::DateTime: - if($var != ''){ + if(is_string($var) && $var != ''){ try{ return new DateTimeImmutable($var); } diff --git a/lib/Image.php b/lib/Image.php index d50d3230..39e30054 100644 --- a/lib/Image.php +++ b/lib/Image.php @@ -17,7 +17,7 @@ class Image{ } /** - * @return resource + * @return \GdImage * @throws \Safe\Exceptions\ImageException * @throws Exceptions\InvalidImageUploadException */ @@ -43,7 +43,7 @@ class Image{ } /** - * @return resource + * @return \GdImage * @throws Exceptions\InvalidImageUploadException */ private function GetImageHandleFromTiff(){ @@ -98,8 +98,8 @@ class Image{ throw new Exceptions\InvalidImageUploadException($ex->getMessage()); } - $imageWidth = $imageDimensions[0]; - $imageHeight = $imageDimensions[1]; + $imageWidth = $imageDimensions[0] ?? 0; + $imageHeight = $imageDimensions[1] ?? 0; if($imageHeight > $imageWidth){ $destinationHeight = $height; diff --git a/lib/Manual.php b/lib/Manual.php index 68c0112a..5bcb56e1 100644 --- a/lib/Manual.php +++ b/lib/Manual.php @@ -15,7 +15,9 @@ class Manual{ public static function GetRequestedVersion(): ?string{ try{ - if(preg_match_all('|/manual/([0-9]+\.[0-9]+\.[0-9]+)|ius', $_SERVER['REQUEST_URI'], $matches)){ + /** @var string $requestUri */ + $requestUri = $_SERVER['REQUEST_URI']; + if(preg_match_all('|/manual/([0-9]+\.[0-9]+\.[0-9]+)|ius', $requestUri, $matches)){ return($matches[1][0]); } else{ diff --git a/lib/Museum.php b/lib/Museum.php index b801422d..d9deb9a7 100644 --- a/lib/Museum.php +++ b/lib/Museum.php @@ -36,6 +36,9 @@ class Museum{ $parsedUrl['path'] = $parsedUrl['path'] ?? ''; + // We've initialized `$parsedUrl`, formally define it for PHPStan. + /** @var array{'path': string, 'host': string, 'fragment'?: string, 'query'?: string} $parsedUrl */ + // We can't match on TLD because extracting the TLD for double-barrel TLDs, like .gov.uk, requires a whitelist. if(preg_match('/\brijksmuseum\.nl$/ius', $parsedUrl['host'])){ @@ -112,10 +115,6 @@ class Museum{ throw new Exceptions\InvalidMuseumUrlException($url, $exampleUrl); } - if($parsedUrl['path'] != '/eMP/eMuseumPlus'){ - throw new Exceptions\InvalidMuseumUrlException($url, $exampleUrl); - } - parse_str($parsedUrl['query'] ?? '', $vars); if(!isset($vars['objectId']) || is_array($vars['objectId'])){ diff --git a/lib/Poll.php b/lib/Poll.php index 830e71fd..771be151 100644 --- a/lib/Poll.php +++ b/lib/Poll.php @@ -15,8 +15,8 @@ class Poll{ public string $UrlName; public string $Description; public DateTimeImmutable $Created; - public DateTimeImmutable $Start; - public DateTimeImmutable $End; + public ?DateTimeImmutable $Start; + public ?DateTimeImmutable $End; protected string $_Url; /** @var array $_PollItems */ diff --git a/lib/PollItem.php b/lib/PollItem.php index 1a841a9e..35ec487d 100644 --- a/lib/PollItem.php +++ b/lib/PollItem.php @@ -9,7 +9,7 @@ class PollItem{ public int $PollItemId; public int $PollId; public string $Name; - public string $Description; + public ?string $Description; protected int $_VoteCount; protected Poll $_Poll; diff --git a/lib/Template.php b/lib/Template.php index ea2fd880..b3ef7960 100644 --- a/lib/Template.php +++ b/lib/Template.php @@ -68,6 +68,7 @@ class Template{ public static function RedirectToLogin(bool $redirectToDestination = true, ?string $destinationUrl = null): void{ if($redirectToDestination){ if($destinationUrl === null){ + /** @var string $destinationUrl */ $destinationUrl = $_SERVER['SCRIPT_URL']; } @@ -81,6 +82,8 @@ class Template{ } public static function IsEreaderBrowser(): bool{ - return isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "Kobo") !== false || strpos($_SERVER['HTTP_USER_AGENT'], "Kindle") !== false); + /** @var string $httpUserAgent */ + $httpUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + return $httpUserAgent != '' && (strpos($httpUserAgent, "Kobo") !== false || strpos($httpUserAgent, "Kindle") !== false); } } diff --git a/scripts/ingest-fa-payments b/scripts/ingest-fa-payments index 0c1460e0..89503cf0 100755 --- a/scripts/ingest-fa-payments +++ b/scripts/ingest-fa-payments @@ -20,6 +20,7 @@ use Facebook\WebDriver\WebDriverElement; use function Safe\file_get_contents; use function Safe\file_put_contents; +use function Safe\get_cfg_var; use function Safe\preg_replace; use function Safe\putenv; use function Safe\set_time_limit; diff --git a/scripts/process-pending-payments b/scripts/process-pending-payments index 3b503dcd..9f407f8d 100755 --- a/scripts/process-pending-payments +++ b/scripts/process-pending-payments @@ -10,6 +10,7 @@ use Facebook\WebDriver\Firefox\FirefoxOptions; use Facebook\WebDriver\WebDriverElement; use Safe\DateTimeImmutable; +use function Safe\get_cfg_var; use function Safe\preg_match; use function Safe\preg_replace; use function Safe\putenv; diff --git a/scripts/update-project-statuses b/scripts/update-project-statuses index 18da45cb..d05e6c51 100755 --- a/scripts/update-project-statuses +++ b/scripts/update-project-statuses @@ -2,8 +2,9 @@ - - Status !== null){ ?> - Status->value) ?> - - EbookUrl !== null){ ?> - — in use - EbookUrl !== null){ ?> - by - Ebook !== null && $artwork->Ebook->Url !== null){ ?> - - Ebook->Title) ?> - Ebook->IsPlaceholder()){ ?>(unreleased) - - EbookUrl) ?> (unreleased) - - +Status->value) ?> +EbookUrl !== null){ ?> + — in use by + Ebook !== null && $artwork->Ebook->Url !== null){ ?> + + Ebook->Title) ?> + Ebook->IsPlaceholder()){ ?>(unreleased) + + EbookUrl) ?> (unreleased) diff --git a/templates/DonationAlert.php b/templates/DonationAlert.php index 60312b63..d59d99a5 100644 --- a/templates/DonationAlert.php +++ b/templates/DonationAlert.php @@ -1,7 +1,7 @@ ` as an undismissable popup. Serve a `
` to Kindle instead. // See . $element = 'aside'; - if(stripos($_SERVER['HTTP_USER_AGENT'] ?? '', 'kindle') !== false){ + /** @var string $httpUserAgent */ + $httpUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + if(stripos($httpUserAgent, 'kindle') !== false){ $element = 'div'; } ?> - < class="donation"> -

We rely on your support to help us keep producing beautiful, free, and unrestricted editions of literature for the digital age.

-

Will you support our efforts with a donation?

- > - +< class="donation"> +

We rely on your support to help us keep producing beautiful, free, and unrestricted editions of literature for the digital age.

+

Will you support our efforts with a donation?

+> diff --git a/templates/Header.php b/templates/Header.php index ac02a339..87a89272 100644 --- a/templates/Header.php +++ b/templates/Header.php @@ -19,7 +19,15 @@ $showPublicDomainDayBanner = PD_NOW > new DateTimeImmutable('January 1, 8:00 AM' // As of Sep. 2022, all versions of Safari have a bug where if the page is served as XHTML, then `` elements download all ``s instead of the first supported match. // So, we try to detect Safari here, and don't use multiple `` if we find Safari. // See . -$isSafari = stripos($_SERVER['HTTP_USER_AGENT'] ?? '', 'safari') !== false; +/** @var string $httpUserAgent */ +$httpUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; +$isSafari = stripos($httpUserAgent, 'safari') !== false; + +if(!$isErrorPage){ + /** @var string $url */ + $url = $_SERVER['ORIG_PATH_INFO'] ?? $_SERVER['SCRIPT_URI'] ?? ''; + $pageUrl = SITE_URL . str_replace(SITE_URL, '', ($url)); +} if(!$isXslt){ if(!$isSafari){ @@ -84,7 +92,7 @@ if(!$isXslt){ - + diff --git a/templates/OpdsNavigationFeed.php b/templates/OpdsNavigationFeed.php index 18316d54..d85a3363 100644 --- a/templates/OpdsNavigationFeed.php +++ b/templates/OpdsNavigationFeed.php @@ -2,7 +2,7 @@ /** * @var string $id * @var string $url - * @var string $parentUrl + * @var ?string $parentUrl * @var string $title * @var ?string $subtitle * @var DateTimeImmutable $updated diff --git a/www/artists/get.php b/www/artists/get.php index 3913077f..6d00d8f4 100644 --- a/www/artists/get.php +++ b/www/artists/get.php @@ -1,6 +1,6 @@ Benefits?->CanReviewArtwork ?? false; -$submitterUserId = Session::$User?->Benefits?->CanUploadArtwork ? Session::$User->UserId : null; +$isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false; +$submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null; $artworkFilterType = Enums\ArtworkFilterType::Approved; diff --git a/www/artworks/index.php b/www/artworks/index.php index 9472e5aa..9f59593c 100644 --- a/www/artworks/index.php +++ b/www/artworks/index.php @@ -11,8 +11,8 @@ $totalArtworkCount = 0; $pageDescription = ''; $pageTitle = ''; $queryString = ''; -$isReviewerView = Session::$User?->Benefits?->CanReviewArtwork ?? false; -$submitterUserId = Session::$User?->Benefits?->CanUploadArtwork ? Session::$User->UserId : null; +$isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false; +$submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null; try{ @@ -127,6 +127,7 @@ catch(Exceptions\ArtworkNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } catch(Exceptions\PageOutOfBoundsException){ + /** @var string $queryStringWithoutPage */ $url = '/artworks?page=' . $pages; if($queryStringWithoutPage != ''){ $url .= '&' . $queryStringWithoutPage; diff --git a/www/bulk-downloads/download.php b/www/bulk-downloads/download.php index 370745f5..60f62adc 100644 --- a/www/bulk-downloads/download.php +++ b/www/bulk-downloads/download.php @@ -32,7 +32,9 @@ try{ } catch(Exceptions\LoginRequiredException){ if(isset($_SERVER['HTTP_REFERER'])){ - Template::RedirectToLogin(true, $_SERVER['HTTP_REFERER']); + /** @var string $httpReferer */ + $httpReferer = $_SERVER['HTTP_REFERER']; + Template::RedirectToLogin(true, $httpReferer); } else{ preg_match('|(^/bulk-downloads/[^/]+?)/|ius', $path, $matches); diff --git a/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php b/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php index 2fceff5b..248421f8 100644 --- a/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php +++ b/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php @@ -1,6 +1,4 @@ - 'How to Create SVGs from Maps with Several Colors', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to producing SVG from images such as maps with more than a single color.']) ?> + 'How to Create SVGs from Maps with Several Colors', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to producing SVG from images such as maps with more than a single color.']) ?>

How to Create SVGs from Maps with Several Colors

diff --git a/www/ebooks/index.php b/www/ebooks/index.php index 107e0595..a26d93a1 100644 --- a/www/ebooks/index.php +++ b/www/ebooks/index.php @@ -80,7 +80,9 @@ try{ ksort($queryStringParams); // If all we did was select one tag, redirect the user to `/subjects/` instead of `/ebooks?tag[0]=`. - if(sizeof($tags) == 1 && $query == '' && preg_match('|^/ebooks|iu', $_SERVER['REQUEST_URI'] ?? '')){ + /** @var string $requestUri */ + $requestUri = $_SERVER['REQUEST_URI'] ?? ''; + if(sizeof($tags) == 1 && $query == '' && preg_match('|^/ebooks|iu', $requestUri)){ unset($queryStringParams['tags']); $queryStringWithoutTags = http_build_query($queryStringParams); $url = '/subjects/' . $tags[0]; diff --git a/www/feeds/401.php b/www/feeds/401.php index 7fa4720a..6e1a325a 100644 --- a/www/feeds/401.php +++ b/www/feeds/401.php @@ -2,7 +2,9 @@ use function Safe\preg_match; $feedType = ''; -preg_match('/^\/feeds\/(opds|rss|atom)/ius', $_SERVER['REQUEST_URI'], $matches); +/** @var string $requestUri */ +$requestUri = $_SERVER['REQUEST_URI'] ?? ''; +preg_match('/^\/feeds\/(opds|rss|atom)/ius', $requestUri, $matches); if(sizeof($matches) > 0){ $feedType = Enums\FeedType::tryFrom(strtolower($matches[1])); diff --git a/www/manual/index.php b/www/manual/index.php index 879c6168..0cf07aeb 100644 --- a/www/manual/index.php +++ b/www/manual/index.php @@ -1,7 +1,6 @@ Start !== null && $poll->Start > NOW){ ?>

This poll opens on Start->format(Enums\DateTimeFormat::FullDateTime->value) ?>.

-

This poll closed on End->format(Enums\DateTimeFormat::FullDateTime->value) ?>.

+ End !== null){ ?> +

This poll closed on End->format(Enums\DateTimeFormat::FullDateTime->value) ?>.

+

View results

diff --git a/www/settings/post.php b/www/settings/post.php index e21c21e9..955af405 100644 --- a/www/settings/post.php +++ b/www/settings/post.php @@ -30,6 +30,7 @@ if($httpMethod == Enums\HttpMethod::Patch){ // HTTP 303, See other http_response_code(Enums\HttpCode::SeeOther->value); + /** @var string $redirect */ $redirect = $_SERVER['HTTP_REFERER'] ?? '/'; header('Location: ' . $redirect); } diff --git a/www/webhooks/github.php b/www/webhooks/github.php index 3a8de844..24f210ab 100644 --- a/www/webhooks/github.php +++ b/www/webhooks/github.php @@ -6,11 +6,13 @@ use function Safe\exec; use function Safe\file_get_contents; use function Safe\json_decode; +use function Safe\get_cfg_var; use function Safe\glob; use function Safe\shell_exec; +$log = new Log(GITHUB_WEBHOOK_LOG_FILE_PATH); + try{ - $log = new Log(GITHUB_WEBHOOK_LOG_FILE_PATH); $lastPushHashFlag = ''; HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]); @@ -20,7 +22,9 @@ try{ $post = file_get_contents('php://input'); // Validate the GitHub secret. - $splitHash = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE']); + /** @var string $githubSignature */ + $githubSignature = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? ''; + $splitHash = explode('=', $githubSignature); $hashAlgorithm = $splitHash[0]; $hash = $splitHash[1]; @@ -80,7 +84,7 @@ try{ // Check the local repo's last commit. If it matches this push, then don't do anything; we're already up to date. - $lastCommitSha1 = trim(shell_exec('git -C ' . escapeshellarg($dir) . ' rev-parse HEAD 2>&1')); + $lastCommitSha1 = trim(shell_exec('git -C ' . escapeshellarg($dir) . ' rev-parse HEAD 2>&1') ?? ''); if($lastCommitSha1 == ''){ $log->Write('Error getting last local commit. Output: ' . $lastCommitSha1); @@ -95,12 +99,11 @@ try{ } // Get the current HEAD hash and save for later. - $output = []; exec('sudo --set-home --user=se-vcs-bot git -C ' . escapeshellarg($dir) . ' rev-parse HEAD', $output, $returnCode); if($returnCode != 0){ $log->Write('Couldn\'t get last commit of local repo. Output: ' . implode("\n", $output)); } - else{ + elseif(sizeof($output ?? []) > 0){ $lastPushHashFlag = ' --last-push-hash ' . escapeshellarg($output[0]); } diff --git a/www/webhooks/postmark.php b/www/webhooks/postmark.php index 60926e65..064a4ba5 100644 --- a/www/webhooks/postmark.php +++ b/www/webhooks/postmark.php @@ -3,10 +3,12 @@ use function Safe\curl_exec; use function Safe\curl_init; use function Safe\curl_setopt; use function Safe\file_get_contents; +use function Safe\get_cfg_var; use function Safe\json_decode; +$log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH); + try{ - $log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH); /** @var string $smtpUsername */ $smtpUsername = get_cfg_var('se.secrets.postmark.username'); @@ -17,7 +19,9 @@ try{ $apiKey = get_cfg_var('se.secrets.postmark.api_key'); // Ensure this webhook actually came from Postmark. - if($apiKey != ($_SERVER['HTTP_X_SE_KEY'] ?? '')){ + /** @var string $postmarkKey */ + $postmarkKey = $_SERVER['HTTP_X_SE_KEY'] ?? ''; + if($apiKey != $postmarkKey){ throw new Exceptions\InvalidCredentialsException(); } @@ -76,7 +80,8 @@ try{ http_response_code(Enums\HttpCode::NoContent->value); } catch(Exceptions\InvalidCredentialsException){ - $log->Write('Invalid key: ' . ($_SERVER['HTTP_X_SE_KEY'] ?? '')); + /** @var string $postmarkKey */ + $log->Write('Invalid key: ' . $postmarkKey); http_response_code(Enums\HttpCode::Forbidden->value); } catch(Exceptions\WebhookException $ex){ diff --git a/www/webhooks/zoho.php b/www/webhooks/zoho.php index 96c48494..c0578614 100644 --- a/www/webhooks/zoho.php +++ b/www/webhooks/zoho.php @@ -1,12 +1,13 @@ Write('Received Zoho webhook.'); @@ -17,7 +18,9 @@ try{ /** @var string $zohoWebhookSecret */ $zohoWebhookSecret = get_cfg_var('se.secrets.zoho.webhook_secret'); - if(!hash_equals($_SERVER['HTTP_X_HOOK_SIGNATURE'], base64_encode(hash_hmac('sha256', $post, $zohoWebhookSecret, true)))){ + /** @var string $zohoHookSignature */ + $zohoHookSignature = $_SERVER['HTTP_X_HOOK_SIGNATURE']; + if(!hash_equals($zohoHookSignature, base64_encode(hash_hmac('sha256', $post, $zohoWebhookSecret, true)))){ throw new Exceptions\InvalidCredentialsException(); }