Update PHPStan and Safe PHP, and review codebase for further type correctness

This commit is contained in:
Alex Cabal 2025-02-25 22:09:35 -06:00
parent e2e14a3551
commit 9d1b66d19e
35 changed files with 301 additions and 169 deletions

View file

@ -20,11 +20,11 @@
] ]
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.11.1", "phpstan/phpstan": "^2.1.6",
"thecodingmachine/phpstan-safe-rule": "^1.2.0" "thecodingmachine/phpstan-safe-rule": "^1.4.0"
}, },
"require": { "require": {
"thecodingmachine/safe": "^2.5.0", "thecodingmachine/safe": "^3.0.2",
"phpmailer/phpmailer": "^6.6.0", "phpmailer/phpmailer": "^6.6.0",
"ramsey/uuid": "^4.7.6", "ramsey/uuid": "^4.7.6",
"gregwar/captcha": "^1.2.0", "gregwar/captcha": "^1.2.0",

173
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b0f9117ed0fa1c3563107eca1d2a77b9", "content-hash": "958c9c0a2d1f5242e7c4952657999fe4",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -550,16 +550,16 @@
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v6.4.13", "version": "v6.4.17",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958" "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/daea9eca0b08d0ed1dc9ab702a46128fd1be4958", "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
"reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958", "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -594,7 +594,7 @@
"description": "Finds files and directories via an intuitive fluent interface", "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/finder/tree/v6.4.13" "source": "https://github.com/symfony/finder/tree/v6.4.17"
}, },
"funding": [ "funding": [
{ {
@ -610,7 +610,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-10-01T08:30:56+00:00" "time": "2024-12-29T13:51:37+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
@ -638,8 +638,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -755,46 +755,31 @@
}, },
{ {
"name": "thecodingmachine/safe", "name": "thecodingmachine/safe",
"version": "v2.5.0", "version": "v3.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thecodingmachine/safe.git", "url": "https://github.com/thecodingmachine/safe.git",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" "reference": "22ffad3248982a784f9870a37aeb2e522bd19645"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/22ffad3248982a784f9870a37aeb2e522bd19645",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "reference": "22ffad3248982a784f9870a37aeb2e522bd19645",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.0" "php": "^8.1"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.5", "php-parallel-lint/php-parallel-lint": "^1.4",
"phpunit/phpunit": "^9.5", "phpstan/phpstan": "^2",
"squizlabs/php_codesniffer": "^3.2", "phpunit/phpunit": "^10",
"thecodingmachine/phpstan-strict-rules": "^1.0" "squizlabs/php_codesniffer": "^3.2"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": { "autoload": {
"files": [ "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", "lib/special_cases.php",
"deprecated/mysqli.php",
"generated/apache.php", "generated/apache.php",
"generated/apcu.php", "generated/apcu.php",
"generated/array.php", "generated/array.php",
@ -834,6 +819,7 @@
"generated/mbstring.php", "generated/mbstring.php",
"generated/misc.php", "generated/misc.php",
"generated/mysql.php", "generated/mysql.php",
"generated/mysqli.php",
"generated/network.php", "generated/network.php",
"generated/oci8.php", "generated/oci8.php",
"generated/opcache.php", "generated/opcache.php",
@ -846,6 +832,7 @@
"generated/ps.php", "generated/ps.php",
"generated/pspell.php", "generated/pspell.php",
"generated/readline.php", "generated/readline.php",
"generated/rnp.php",
"generated/rpminfo.php", "generated/rpminfo.php",
"generated/rrd.php", "generated/rrd.php",
"generated/sem.php", "generated/sem.php",
@ -877,7 +864,6 @@
"lib/DateTime.php", "lib/DateTime.php",
"lib/DateTimeImmutable.php", "lib/DateTimeImmutable.php",
"lib/Exceptions/", "lib/Exceptions/",
"deprecated/Exceptions/",
"generated/Exceptions/" "generated/Exceptions/"
] ]
}, },
@ -888,28 +874,100 @@
"description": "PHP core functions that throw exceptions instead of returning FALSE on error", "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": { "support": {
"issues": "https://github.com/thecodingmachine/safe/issues", "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": [ "packages-dev": [
{ {
"name": "phpstan/phpstan", "name": "nikic/php-parser",
"version": "1.12.11", "version": "v5.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": "" "shasum": ""
}, },
"require": { "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": { "conflict": {
"phpstan/phpstan-shim": "*" "phpstan/phpstan-shim": "*"
@ -950,41 +1008,42 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-11-17T14:08:01+00:00" "time": "2025-02-19T15:46:42+00:00"
}, },
{ {
"name": "thecodingmachine/phpstan-safe-rule", "name": "thecodingmachine/phpstan-safe-rule",
"version": "v1.2.0", "version": "v1.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git",
"reference": "8a7b88e0d54f209a488095085f183e9174c40e1e" "reference": "33dcbc3228c55ea4c364ecf74a3661cf7b7f168d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/8a7b88e0d54f209a488095085f183e9174c40e1e", "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/33dcbc3228c55ea4c364ecf74a3661cf7b7f168d",
"reference": "8a7b88e0d54f209a488095085f183e9174c40e1e", "reference": "33dcbc3228c55ea4c364ecf74a3661cf7b7f168d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1 || ^8.0", "nikic/php-parser": "^5",
"phpstan/phpstan": "^1.0", "php": "^8.1",
"thecodingmachine/safe": "^1.0 || ^2.0" "phpstan/phpstan": "^2.0",
"thecodingmachine/safe": "^1.2 || ^2.0 || ^3.0"
}, },
"require-dev": { "require-dev": {
"php-coveralls/php-coveralls": "^2.1", "php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^7.5.2 || ^8.0", "phpunit/phpunit": "^10.4",
"squizlabs/php_codesniffer": "^3.4" "squizlabs/php_codesniffer": "^3.4"
}, },
"type": "phpstan-extension", "type": "phpstan-extension",
"extra": { "extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"phpstan": { "phpstan": {
"includes": [ "includes": [
"phpstan-safe-rule.neon" "phpstan-safe-rule.neon"
] ]
},
"branch-alias": {
"dev-master": "2.0-dev"
} }
}, },
"autoload": { "autoload": {
@ -1005,9 +1064,9 @@
"description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe",
"support": { "support": {
"issues": "https://github.com/thecodingmachine/phpstan-safe-rule/issues", "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": [], "aliases": [],

View file

@ -40,7 +40,6 @@ parameters:
-'#^Safe\\#' -'#^Safe\\#'
uncheckedExceptionClasses: uncheckedExceptionClasses:
- 'Exceptions\DatabaseQueryException' - 'Exceptions\DatabaseQueryException'
- 'Exceptions\DuplicateDatabaseKeyException'
- 'Exceptions\MultiSelectMethodNotFoundException' - 'Exceptions\MultiSelectMethodNotFoundException'
- 'PDOException' - 'PDOException'
- 'TypeError' - 'TypeError'

View file

@ -254,7 +254,7 @@ class Artwork{
if(!isset($this->Dimensions)){ if(!isset($this->Dimensions)){
$this->_Dimensions = ''; $this->_Dimensions = '';
try{ try{
list($imageWidth, $imageHeight) = getimagesize($this->ImageFsPath); list($imageWidth, $imageHeight) = (getimagesize($this->ImageFsPath) ?? throw new \Exception());
if($imageWidth && $imageHeight){ if($imageWidth && $imageHeight){
$this->_Dimensions = number_format($imageWidth) . ' × ' . number_format($imageHeight); $this->_Dimensions = number_format($imageWidth) . ' × ' . number_format($imageHeight);
} }
@ -529,11 +529,16 @@ class Artwork{
} }
// Check for minimum dimensions. // Check for minimum dimensions.
list($imageWidth, $imageHeight) = getimagesize($imagePath); try{
list($imageWidth, $imageHeight) = (getimagesize($imagePath) ?? throw new \Exception());
if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){ if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){
$error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException()); $error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException());
} }
} }
catch(\Exception){
$error->Add(new Exceptions\InvalidImageUploadException());
}
}
} }
if(!isset($this->MimeType)){ if(!isset($this->MimeType)){
@ -647,7 +652,7 @@ class Artwork{
} }
preg_match('|^/books/edition/[^/]+/([^/]+)$|ius', $parsedUrl['path'], $matches); preg_match('|^/books/edition/[^/]+/([^/]+)$|ius', $parsedUrl['path'], $matches);
$id = $matches[1]; $id = $matches[1] ?? '';
parse_str($parsedUrl['query'] ?? '', $vars); parse_str($parsedUrl['query'] ?? '', $vars);

View file

@ -61,11 +61,9 @@ class AtomFeed extends Feed{
} }
} }
else{ else{
if($entry->Updated !== null){
$obj->Updated = $entry->Updated->format(Enums\DateTimeFormat::Iso->value); $obj->Updated = $entry->Updated->format(Enums\DateTimeFormat::Iso->value);
$obj->Id = $entry->Id; $obj->Id = $entry->Id;
} }
}
if(isset($obj->Id)){ if(isset($obj->Id)){
$currentEntries[] = $obj; $currentEntries[] = $obj;

View file

@ -1,6 +1,7 @@
<? <?
// Auto-included by Composer in `composer.json` to satisfy PHPStan. // Auto-included by Composer in `composer.json` to satisfy PHPStan.
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
use function Safe\get_cfg_var;
use function Safe\define; use function Safe\define;
const NOW = new DateTimeImmutable(); const NOW = new DateTimeImmutable();

View file

@ -71,6 +71,7 @@ Db::Connect(DATABASE_DEFAULT_DATABASE, DATABASE_DEFAULT_HOST);
Session::InitializeFromCookie(); Session::InitializeFromCookie();
if(Session::$User === null){ if(Session::$User === null){
/** @var ?string $httpBasicAuthLogin */
$httpBasicAuthLogin = $_SERVER['PHP_AUTH_USER'] ?? null; $httpBasicAuthLogin = $_SERVER['PHP_AUTH_USER'] ?? null;
if($httpBasicAuthLogin !== null){ if($httpBasicAuthLogin !== null){
@ -78,6 +79,7 @@ if(Session::$User === null){
$session = new Session(); $session = new Session();
try{ try{
/** @var ?string $password */
$password = $_SERVER['PHP_AUTH_PW'] ?? null; $password = $_SERVER['PHP_AUTH_PW'] ?? null;
if($password == ''){ if($password == ''){
$password = null; $password = null;

View file

@ -465,7 +465,7 @@ final class Ebook{
return $this->_HeroImageUrl; return $this->_HeroImageUrl;
} }
protected function GetHeroImageAvifUrl(): ?string{ protected function GetHeroImageAvifUrl(): string{
if(!isset($this->_HeroImageAvifUrl)){ if(!isset($this->_HeroImageAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero.avif')){ 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'; $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; return $this->_HeroImage2xUrl;
} }
protected function GetHeroImage2xAvifUrl(): ?string{ protected function GetHeroImage2xAvifUrl(): string{
if(!isset($this->_HeroImage2xAvifUrl)){ if(!isset($this->_HeroImage2xAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero@2x.avif')){ 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'; $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; return $this->_CoverImageUrl;
} }
protected function GetCoverImageAvifUrl(): ?string{ protected function GetCoverImageAvifUrl(): string{
if(!isset($this->_CoverImageAvifUrl)){ if(!isset($this->_CoverImageAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover.avif')){ 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'; $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; return $this->_CoverImage2xUrl;
} }
protected function GetCoverImage2xAvifUrl(): ?string{ protected function GetCoverImage2xAvifUrl(): string{
if(!isset($this->_CoverImage2xAvifUrl)){ if(!isset($this->_CoverImage2xAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover@2x.avif')){ 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'; $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. // Fill in the short history of this repo.
try{ 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 = []; $gitCommits = [];
foreach($historyEntries as $logLine){ 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"]')); $ebook->AlternateTitle = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="dcterms:alternate"][@refines="#title"]'));
$date = $xml->xpath('/package/metadata/dc:date') ?: []; $date = $xml->xpath('/package/metadata/dc:date') ?: [];
if($date !== false && sizeof($date) > 0){ if(sizeof($date) > 0){
/** @throws void */ /** @throws void */
$ebook->EbookCreated = new DateTimeImmutable((string)$date[0]); $ebook->EbookCreated = new DateTimeImmutable((string)$date[0]);
} }
$modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]') ?: []; $modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]') ?: [];
if($modifiedDate !== false && sizeof($modifiedDate) > 0){ if(sizeof($modifiedDate) > 0){
/** @throws void */ /** @throws void */
$ebook->EbookUpdated = new DateTimeImmutable((string)$modifiedDate[0]); $ebook->EbookUpdated = new DateTimeImmutable((string)$modifiedDate[0]);
} }
@ -949,7 +949,7 @@ final class Ebook{
$fileAs = null; $fileAs = null;
$fileAsElement = $xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]') ?: []; $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]; $fileAs = (string)$fileAsElement[0];
} }
else{ else{
@ -1030,15 +1030,15 @@ final class Ebook{
$ebook->Language = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:language')) ?? ''; $ebook->Language = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:language')) ?? '';
$wordCount = 0; $wordCount = 0;
$wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]'); $wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]') ?: [];
if($wordCountElement !== false && sizeof($wordCountElement) > 0){ if(sizeof($wordCountElement) > 0){
$wordCount = (int)$wordCountElement[0]; $wordCount = (int)$wordCountElement[0];
} }
$ebook->WordCount = $wordCount; $ebook->WordCount = $wordCount;
$readingEase = 0; $readingEase = 0;
$readingEaseElement = $xml->xpath('/package/metadata/meta[@property="se:reading-ease.flesch"]'); $readingEaseElement = $xml->xpath('/package/metadata/meta[@property="se:reading-ease.flesch"]') ?: [];
if($readingEaseElement !== false && sizeof($readingEaseElement) > 0){ if(sizeof($readingEaseElement) > 0){
$readingEase = (float)$readingEaseElement[0]; $readingEase = (float)$readingEaseElement[0];
} }
$ebook->ReadingEase = $readingEase; $ebook->ReadingEase = $readingEase;
@ -1124,7 +1124,7 @@ final class Ebook{
*/ */
protected static function MatchContributorUrlNameToIdentifier(string $urlName, string $identifier): string{ protected static function MatchContributorUrlNameToIdentifier(string $urlName, string $identifier): string{
if(preg_match('|' . $urlName . '[^\/_]*|ius', $identifier, $matches)){ if(preg_match('|' . $urlName . '[^\/_]*|ius', $identifier, $matches)){
return $matches[0]; return $matches[0] ?? '';
} }
else{ else{
return $urlName; return $urlName;

View file

@ -45,7 +45,7 @@ class Email{
$phpMailer->AddAddress($this->To, $this->ToName); $phpMailer->AddAddress($this->To, $this->ToName);
$phpMailer->Subject = $this->Subject; $phpMailer->Subject = $this->Subject;
$phpMailer->CharSet = 'UTF-8'; $phpMailer->CharSet = 'UTF-8';
if($this->TextBody !== null && $this->TextBody != ''){ if($this->TextBody != ''){
$phpMailer->IsHTML(true); $phpMailer->IsHTML(true);
$phpMailer->Body = $this->Body; $phpMailer->Body = $this->Body;
$phpMailer->AltBody = $this->TextBody; $phpMailer->AltBody = $this->TextBody;
@ -55,10 +55,8 @@ class Email{
} }
foreach($this->Attachments as $attachment){ foreach($this->Attachments as $attachment){
if(is_array($attachment)){
$phpMailer->addStringAttachment($attachment['contents'], $attachment['filename']); $phpMailer->addStringAttachment($attachment['contents'], $attachment['filename']);
} }
}
$phpMailer->IsSMTP(); $phpMailer->IsSMTP();
$phpMailer->SMTPAuth = true; $phpMailer->SMTPAuth = true;

View file

@ -1,6 +1,7 @@
<? <?
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
use function Safe\file_get_contents;
use function Safe\ini_get; use function Safe\ini_get;
use function Safe\glob; use function Safe\glob;
use function Safe\preg_match; use function Safe\preg_match;
@ -8,6 +9,31 @@ use function Safe\preg_replace;
use function Safe\mb_convert_encoding; use function Safe\mb_convert_encoding;
class HttpInput{ class HttpInput{
/**
* If we received a `DELETE`, `PATCH`, or `PUT` request, parse the request body into `$_POST`.
*
* This can't handle file uploads, which due to PHP limitations *must* be sent via `POST`.
*/
public static function Initialize(): void{
/** @var string $contentType */
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if(
isset($_SERVER['REQUEST_METHOD'])
&&
(
$_SERVER['REQUEST_METHOD'] == Enums\HttpMethod::Delete->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 `<METHOD>.php` and exit. * Calculate the HTTP method of the request, then include `<METHOD>.php` and exit.
*/ */
@ -22,10 +48,11 @@ class HttpInput{
} }
if($httpMethod == Enums\HttpMethod::Post){ 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; return;
} }
/** @phpstan-ignore-next-line */
include($filename); include($filename);
exit(); exit();
@ -47,11 +74,13 @@ class HttpInput{
* *
* @param ?array<Enums\HttpMethod> $allowedHttpMethods An array containing a list of allowed HTTP methods, or null if any valid HTTP method is allowed. * @param ?array<Enums\HttpMethod> $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. * @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{ public static function ValidateRequestMethod(?array $allowedHttpMethods = null, bool $throwException = false): Enums\HttpMethod{
try{ 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){ if($allowedHttpMethods !== null){
$isRequestMethodAllowed = false; $isRequestMethodAllowed = false;
foreach($allowedHttpMethods as $allowedHttpMethod){ foreach($allowedHttpMethods as $allowedHttpMethod){
@ -65,10 +94,15 @@ class HttpInput{
} }
} }
} }
catch(\ValueError | Exceptions\HttpMethodNotAllowedException){ catch(\ValueError | Exceptions\HttpMethodNotAllowedException $ex){
if($throwException){ if($throwException){
if($ex instanceof \ValueError){
throw new Exceptions\HttpMethodNotAllowedException(); throw new Exceptions\HttpMethodNotAllowedException();
} }
else{
throw $ex;
}
}
else{ else{
if($allowedHttpMethods !== null){ if($allowedHttpMethods !== null){
header('Allow: ' . implode(',', array_map(fn($httpMethod): string => $httpMethod->value, $allowedHttpMethods))); header('Allow: ' . implode(',', array_map(fn($httpMethod): string => $httpMethod->value, $allowedHttpMethods)));
@ -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{ public static function GetMaxPostSize(): int{
$post_max_size = ini_get('upload_max_filesize'); $post_max_size = ini_get('upload_max_filesize');
@ -106,6 +140,7 @@ class HttpInput{
elseif(sizeof($_FILES) > 0){ elseif(sizeof($_FILES) > 0){
// We received files but may have an error because the size exceeded our limit. // We received files but may have an error because the size exceeded our limit.
foreach($_FILES as $file){ foreach($_FILES as $file){
/** @var array<string, int> $file */
$error = $file['error'] ?? UPLOAD_ERR_OK; $error = $file['error'] ?? UPLOAD_ERR_OK;
if($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE){ if($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE){
@ -118,7 +153,9 @@ class HttpInput{
} }
public static function GetRequestType(): Enums\HttpRequestType{ 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; $object = $_SESSION[$variable] ?? null;
if($object !== null){ if(is_object($object)){
foreach($class as $c){ foreach($class as $c){
if(is_a($object, $c)){ if(is_a($object, $c)){
return $object; return $object;
@ -200,12 +237,17 @@ class HttpInput{
public static function File(string $variable): ?string{ public static function File(string $variable): ?string{
$filePath = null; $filePath = null;
if(isset($_FILES[$variable]) && $_FILES[$variable]['size'] > 0){ if(isset($_FILES[$variable])){
if(!is_uploaded_file($_FILES[$variable]['tmp_name']) || $_FILES[$variable]['error'] > UPLOAD_ERR_OK){ /** @var array{'error': int, 'size': int, 'tmp_name': string} $file */
$file = $_FILES[$variable];
if($file['size'] > 0){
if(!is_uploaded_file($file['tmp_name']) || $file['error'] > UPLOAD_ERR_OK){
throw new Exceptions\InvalidFileUploadException(); throw new Exceptions\InvalidFileUploadException();
} }
$filePath = $_FILES[$variable]['tmp_name'] ?? null; $filePath = $file['tmp_name'];
}
} }
return $filePath; return $filePath;
@ -221,11 +263,9 @@ class HttpInput{
} }
/** /**
* @return array<string>|array<int>|array<float>|array<bool>|string|int|float|bool|DateTimeImmutable|null * @return array<string>|array<int>|array<float>|array<bool>|array<mixed>|string|int|float|bool|DateTimeImmutable|null
*/ */
private static function GetHttpVar(string $variable, Enums\HttpVariableType $type, Enums\HttpVariableSource $set): mixed{ 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 = []; $vars = [];
switch($set){ switch($set){
@ -262,6 +302,10 @@ class HttpInput{
switch($type){ switch($type){
case Enums\HttpVariableType::String: case Enums\HttpVariableType::String:
if(!is_string($var)){
return '';
}
// Attempt to fix broken UTF8 strings, often passed by bots and scripts. // Attempt to fix broken UTF8 strings, often passed by bots and scripts.
// Broken UTF8 can cause exceptions in functions like `preg_replace()`. // Broken UTF8 can cause exceptions in functions like `preg_replace()`.
try{ try{
@ -282,7 +326,7 @@ class HttpInput{
} }
break; break;
case Enums\HttpVariableType::Boolean: 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; return false;
} }
else{ else{
@ -299,7 +343,7 @@ class HttpInput{
} }
break; break;
case Enums\HttpVariableType::DateTime: case Enums\HttpVariableType::DateTime:
if($var != ''){ if(is_string($var) && $var != ''){
try{ try{
return new DateTimeImmutable($var); return new DateTimeImmutable($var);
} }

View file

@ -17,7 +17,7 @@ class Image{
} }
/** /**
* @return resource * @return \GdImage
* @throws \Safe\Exceptions\ImageException * @throws \Safe\Exceptions\ImageException
* @throws Exceptions\InvalidImageUploadException * @throws Exceptions\InvalidImageUploadException
*/ */
@ -43,7 +43,7 @@ class Image{
} }
/** /**
* @return resource * @return \GdImage
* @throws Exceptions\InvalidImageUploadException * @throws Exceptions\InvalidImageUploadException
*/ */
private function GetImageHandleFromTiff(){ private function GetImageHandleFromTiff(){
@ -98,8 +98,8 @@ class Image{
throw new Exceptions\InvalidImageUploadException($ex->getMessage()); throw new Exceptions\InvalidImageUploadException($ex->getMessage());
} }
$imageWidth = $imageDimensions[0]; $imageWidth = $imageDimensions[0] ?? 0;
$imageHeight = $imageDimensions[1]; $imageHeight = $imageDimensions[1] ?? 0;
if($imageHeight > $imageWidth){ if($imageHeight > $imageWidth){
$destinationHeight = $height; $destinationHeight = $height;

View file

@ -15,7 +15,9 @@ class Manual{
public static function GetRequestedVersion(): ?string{ public static function GetRequestedVersion(): ?string{
try{ 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]); return($matches[1][0]);
} }
else{ else{

View file

@ -36,6 +36,9 @@ class Museum{
$parsedUrl['path'] = $parsedUrl['path'] ?? ''; $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. // 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'])){ if(preg_match('/\brijksmuseum\.nl$/ius', $parsedUrl['host'])){
@ -112,10 +115,6 @@ class Museum{
throw new Exceptions\InvalidMuseumUrlException($url, $exampleUrl); throw new Exceptions\InvalidMuseumUrlException($url, $exampleUrl);
} }
if($parsedUrl['path'] != '/eMP/eMuseumPlus'){
throw new Exceptions\InvalidMuseumUrlException($url, $exampleUrl);
}
parse_str($parsedUrl['query'] ?? '', $vars); parse_str($parsedUrl['query'] ?? '', $vars);
if(!isset($vars['objectId']) || is_array($vars['objectId'])){ if(!isset($vars['objectId']) || is_array($vars['objectId'])){

View file

@ -15,8 +15,8 @@ class Poll{
public string $UrlName; public string $UrlName;
public string $Description; public string $Description;
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public DateTimeImmutable $Start; public ?DateTimeImmutable $Start;
public DateTimeImmutable $End; public ?DateTimeImmutable $End;
protected string $_Url; protected string $_Url;
/** @var array<PollItem> $_PollItems */ /** @var array<PollItem> $_PollItems */

View file

@ -9,7 +9,7 @@ class PollItem{
public int $PollItemId; public int $PollItemId;
public int $PollId; public int $PollId;
public string $Name; public string $Name;
public string $Description; public ?string $Description;
protected int $_VoteCount; protected int $_VoteCount;
protected Poll $_Poll; protected Poll $_Poll;

View file

@ -68,6 +68,7 @@ class Template{
public static function RedirectToLogin(bool $redirectToDestination = true, ?string $destinationUrl = null): void{ public static function RedirectToLogin(bool $redirectToDestination = true, ?string $destinationUrl = null): void{
if($redirectToDestination){ if($redirectToDestination){
if($destinationUrl === null){ if($destinationUrl === null){
/** @var string $destinationUrl */
$destinationUrl = $_SERVER['SCRIPT_URL']; $destinationUrl = $_SERVER['SCRIPT_URL'];
} }
@ -81,6 +82,8 @@ class Template{
} }
public static function IsEreaderBrowser(): bool{ 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);
} }
} }

View file

@ -20,6 +20,7 @@ use Facebook\WebDriver\WebDriverElement;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\file_put_contents; use function Safe\file_put_contents;
use function Safe\get_cfg_var;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\putenv; use function Safe\putenv;
use function Safe\set_time_limit; use function Safe\set_time_limit;

View file

@ -10,6 +10,7 @@ use Facebook\WebDriver\Firefox\FirefoxOptions;
use Facebook\WebDriver\WebDriverElement; use Facebook\WebDriver\WebDriverElement;
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
use function Safe\get_cfg_var;
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\putenv; use function Safe\putenv;

View file

@ -2,8 +2,9 @@
<? <?
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
use function Safe\file_get_contents;
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
use function Safe\file_get_contents;
use function Safe\sleep;
/** /**
* Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit. * Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit.

View file

@ -3,14 +3,9 @@
* @var Artwork $artwork * @var Artwork $artwork
*/ */
?> ?>
<? if($artwork !== null){ ?>
<? if($artwork->Status !== null){ ?>
<?= ucfirst($artwork->Status->value) ?> <?= ucfirst($artwork->Status->value) ?>
<? } ?>
<? if($artwork->EbookUrl !== null){ ?> <? if($artwork->EbookUrl !== null){ ?>
in use in use by
<? if($artwork->EbookUrl !== null){ ?>
by
<? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?> <? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?>
<i> <i>
<a href="<?= $artwork->Ebook->Url ?>"><?= Formatter::EscapeHtml($artwork->Ebook->Title) ?></a> <a href="<?= $artwork->Ebook->Url ?>"><?= Formatter::EscapeHtml($artwork->Ebook->Title) ?></a>
@ -19,5 +14,3 @@
<code><?= Formatter::EscapeHtml($artwork->EbookUrl) ?></code> (unreleased) <code><?= Formatter::EscapeHtml($artwork->EbookUrl) ?></code> (unreleased)
<? } ?> <? } ?>
<? } ?> <? } ?>
<? } ?>
<? } ?>

View file

@ -1,7 +1,7 @@
<? <?
// Hide this alert if...
$donationDrive = DonationDrive::GetByIsRunning(); $donationDrive = DonationDrive::GetByIsRunning();
// Hide this alert if...
if( if(
Session::$User !== null // If a user is logged in. Session::$User !== null // If a user is logged in.
|| ||
@ -12,12 +12,13 @@ if(
return; return;
} }
if(Session::$User === null){
// The Kindle browsers renders `<aside>` as an undismissable popup. Serve a `<div>` to Kindle instead. // The Kindle browsers renders `<aside>` as an undismissable popup. Serve a `<div>` to Kindle instead.
// See <https://github.com/standardebooks/web/issues/204>. // See <https://github.com/standardebooks/web/issues/204>.
$element = 'aside'; $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'; $element = 'div';
} }
?> ?>
@ -25,4 +26,3 @@ if(Session::$User === null){
<p>We rely on your support to help us keep producing beautiful, free, and unrestricted editions of literature for the digital age.</p> <p>We rely on your support to help us keep producing beautiful, free, and unrestricted editions of literature for the digital age.</p>
<p>Will you <a href="/donate">support our efforts with a donation</a>?</p> <p>Will you <a href="/donate">support our efforts with a donation</a>?</p>
</<?= $element ?>> </<?= $element ?>>
<? } ?>

View file

@ -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 `<picture>` elements download all `<source>`s instead of the first supported match. // As of Sep. 2022, all versions of Safari have a bug where if the page is served as XHTML, then `<picture>` elements download all `<source>`s instead of the first supported match.
// So, we try to detect Safari here, and don't use multiple `<source>` if we find Safari. // So, we try to detect Safari here, and don't use multiple `<source>` if we find Safari.
// See <https://bugs.webkit.org/show_bug.cgi?id=245411>. // See <https://bugs.webkit.org/show_bug.cgi?id=245411>.
$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(!$isXslt){
if(!$isSafari){ if(!$isSafari){
@ -84,7 +92,7 @@ if(!$isXslt){
<meta content="#394451" name="theme-color"/> <meta content="#394451" name="theme-color"/>
<meta content="<? if($title != ''){ ?><?= Formatter::EscapeHtml($title) ?><? }else{ ?>Standard Ebooks<? } ?>" property="og:title"/> <meta content="<? if($title != ''){ ?><?= Formatter::EscapeHtml($title) ?><? }else{ ?>Standard Ebooks<? } ?>" property="og:title"/>
<meta content="<?= $ogType ?? 'website' ?>" property="og:type"/> <meta content="<?= $ogType ?? 'website' ?>" property="og:type"/>
<meta content="<?= SITE_URL . str_replace(SITE_URL, '', ($_SERVER['ORIG_PATH_INFO'] ?? $_SERVER['SCRIPT_URI'] ?? '')) ?>" property="og:url"/> <meta content="<?= $pageUrl ?>" property="og:url"/>
<meta content="<?= SITE_URL . ($coverUrl ?? '/images/logo.png') ?>" property="og:image"/> <meta content="<?= SITE_URL . ($coverUrl ?? '/images/logo.png') ?>" property="og:image"/>
<meta content="summary_large_image" name="twitter:card"/> <meta content="summary_large_image" name="twitter:card"/>
<meta content="@standardebooks" name="twitter:site"/> <meta content="@standardebooks" name="twitter:site"/>

View file

@ -2,7 +2,7 @@
/** /**
* @var string $id * @var string $id
* @var string $url * @var string $url
* @var string $parentUrl * @var ?string $parentUrl
* @var string $title * @var string $title
* @var ?string $subtitle * @var ?string $subtitle
* @var DateTimeImmutable $updated * @var DateTimeImmutable $updated

View file

@ -1,6 +1,6 @@
<? <?
$isReviewerView = Session::$User?->Benefits?->CanReviewArtwork ?? false; $isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false;
$submitterUserId = Session::$User?->Benefits?->CanUploadArtwork ? Session::$User->UserId : null; $submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null;
$isSubmitterView = !$isReviewerView && $submitterUserId !== null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null;
$artworkFilterType = Enums\ArtworkFilterType::Approved; $artworkFilterType = Enums\ArtworkFilterType::Approved;

View file

@ -11,8 +11,8 @@ $totalArtworkCount = 0;
$pageDescription = ''; $pageDescription = '';
$pageTitle = ''; $pageTitle = '';
$queryString = ''; $queryString = '';
$isReviewerView = Session::$User?->Benefits?->CanReviewArtwork ?? false; $isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false;
$submitterUserId = Session::$User?->Benefits?->CanUploadArtwork ? Session::$User->UserId : null; $submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null;
$isSubmitterView = !$isReviewerView && $submitterUserId !== null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null;
try{ try{
@ -127,6 +127,7 @@ catch(Exceptions\ArtworkNotFoundException){
Template::ExitWithCode(Enums\HttpCode::NotFound); Template::ExitWithCode(Enums\HttpCode::NotFound);
} }
catch(Exceptions\PageOutOfBoundsException){ catch(Exceptions\PageOutOfBoundsException){
/** @var string $queryStringWithoutPage */
$url = '/artworks?page=' . $pages; $url = '/artworks?page=' . $pages;
if($queryStringWithoutPage != ''){ if($queryStringWithoutPage != ''){
$url .= '&' . $queryStringWithoutPage; $url .= '&' . $queryStringWithoutPage;

View file

@ -32,7 +32,9 @@ try{
} }
catch(Exceptions\LoginRequiredException){ catch(Exceptions\LoginRequiredException){
if(isset($_SERVER['HTTP_REFERER'])){ if(isset($_SERVER['HTTP_REFERER'])){
Template::RedirectToLogin(true, $_SERVER['HTTP_REFERER']); /** @var string $httpReferer */
$httpReferer = $_SERVER['HTTP_REFERER'];
Template::RedirectToLogin(true, $httpReferer);
} }
else{ else{
preg_match('|(^/bulk-downloads/[^/]+?)/|ius', $path, $matches); preg_match('|(^/bulk-downloads/[^/]+?)/|ius', $path, $matches);

View file

@ -1,6 +1,4 @@
<? <?= Template::Header(['title' => '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.']) ?>
require_once('Core.php');
?><?= Template::Header(['title' => '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.']) ?>
<main class="manual"> <main class="manual">
<article class="step-by-step-guide"> <article class="step-by-step-guide">
<h1>How to Create SVGs from Maps with Several Colors</h1> <h1>How to Create SVGs from Maps with Several Colors</h1>

View file

@ -80,7 +80,9 @@ try{
ksort($queryStringParams); ksort($queryStringParams);
// If all we did was select one tag, redirect the user to `/subjects/<TAG>` instead of `/ebooks?tag[0]=<TAG>`. // If all we did was select one tag, redirect the user to `/subjects/<TAG>` instead of `/ebooks?tag[0]=<TAG>`.
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']); unset($queryStringParams['tags']);
$queryStringWithoutTags = http_build_query($queryStringParams); $queryStringWithoutTags = http_build_query($queryStringParams);
$url = '/subjects/' . $tags[0]; $url = '/subjects/' . $tags[0];

View file

@ -2,7 +2,9 @@
use function Safe\preg_match; use function Safe\preg_match;
$feedType = ''; $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){ if(sizeof($matches) > 0){
$feedType = Enums\FeedType::tryFrom(strtolower($matches[1])); $feedType = Enums\FeedType::tryFrom(strtolower($matches[1]));

View file

@ -1,7 +1,6 @@
<? <?
use function Safe\glob; use function Safe\glob;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\sort;
// Redirect to the latest version of the manual // Redirect to the latest version of the manual

View file

@ -48,7 +48,9 @@ catch(Exceptions\AppException){
<? if($poll->Start !== null && $poll->Start > NOW){ ?> <? if($poll->Start !== null && $poll->Start > NOW){ ?>
<p class="center-notice">This poll opens on <?= $poll->Start->format(Enums\DateTimeFormat::FullDateTime->value) ?>.</p> <p class="center-notice">This poll opens on <?= $poll->Start->format(Enums\DateTimeFormat::FullDateTime->value) ?>.</p>
<? }else{ ?> <? }else{ ?>
<? if($poll->End !== null){ ?>
<p class="center-notice">This poll closed on <?= $poll->End->format(Enums\DateTimeFormat::FullDateTime->value) ?>.</p> <p class="center-notice">This poll closed on <?= $poll->End->format(Enums\DateTimeFormat::FullDateTime->value) ?>.</p>
<? } ?>
<p class="button-row narrow"><a href="<?= $poll->Url ?>/votes" class="button">View results</a></p> <p class="button-row narrow"><a href="<?= $poll->Url ?>/votes" class="button">View results</a></p>
<? } ?> <? } ?>
<? } ?> <? } ?>

View file

@ -30,6 +30,7 @@ if($httpMethod == Enums\HttpMethod::Patch){
// HTTP 303, See other // HTTP 303, See other
http_response_code(Enums\HttpCode::SeeOther->value); http_response_code(Enums\HttpCode::SeeOther->value);
/** @var string $redirect */
$redirect = $_SERVER['HTTP_REFERER'] ?? '/'; $redirect = $_SERVER['HTTP_REFERER'] ?? '/';
header('Location: ' . $redirect); header('Location: ' . $redirect);
} }

View file

@ -6,11 +6,13 @@
use function Safe\exec; use function Safe\exec;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\json_decode; use function Safe\json_decode;
use function Safe\get_cfg_var;
use function Safe\glob; use function Safe\glob;
use function Safe\shell_exec; use function Safe\shell_exec;
try{
$log = new Log(GITHUB_WEBHOOK_LOG_FILE_PATH); $log = new Log(GITHUB_WEBHOOK_LOG_FILE_PATH);
try{
$lastPushHashFlag = ''; $lastPushHashFlag = '';
HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]); HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]);
@ -20,7 +22,9 @@ try{
$post = file_get_contents('php://input'); $post = file_get_contents('php://input');
// Validate the GitHub secret. // 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]; $hashAlgorithm = $splitHash[0];
$hash = $splitHash[1]; $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. // 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 == ''){ if($lastCommitSha1 == ''){
$log->Write('Error getting last local commit. Output: ' . $lastCommitSha1); $log->Write('Error getting last local commit. Output: ' . $lastCommitSha1);
@ -95,12 +99,11 @@ try{
} }
// Get the current HEAD hash and save for later. // 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); exec('sudo --set-home --user=se-vcs-bot git -C ' . escapeshellarg($dir) . ' rev-parse HEAD', $output, $returnCode);
if($returnCode != 0){ if($returnCode != 0){
$log->Write('Couldn\'t get last commit of local repo. Output: ' . implode("\n", $output)); $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]); $lastPushHashFlag = ' --last-push-hash ' . escapeshellarg($output[0]);
} }

View file

@ -3,10 +3,12 @@ use function Safe\curl_exec;
use function Safe\curl_init; use function Safe\curl_init;
use function Safe\curl_setopt; use function Safe\curl_setopt;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\get_cfg_var;
use function Safe\json_decode; use function Safe\json_decode;
try{
$log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH); $log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH);
try{
/** @var string $smtpUsername */ /** @var string $smtpUsername */
$smtpUsername = get_cfg_var('se.secrets.postmark.username'); $smtpUsername = get_cfg_var('se.secrets.postmark.username');
@ -17,7 +19,9 @@ try{
$apiKey = get_cfg_var('se.secrets.postmark.api_key'); $apiKey = get_cfg_var('se.secrets.postmark.api_key');
// Ensure this webhook actually came from Postmark. // 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(); throw new Exceptions\InvalidCredentialsException();
} }
@ -76,7 +80,8 @@ try{
http_response_code(Enums\HttpCode::NoContent->value); http_response_code(Enums\HttpCode::NoContent->value);
} }
catch(Exceptions\InvalidCredentialsException){ 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); http_response_code(Enums\HttpCode::Forbidden->value);
} }
catch(Exceptions\WebhookException $ex){ catch(Exceptions\WebhookException $ex){

View file

@ -1,12 +1,13 @@
<? <?
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\get_cfg_var;
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\json_decode; use function Safe\json_decode;
// This webhook receives POSTs when email from a Fractured Atlas donation is received at the SE Zoho email account. This script processes the email, and inserts the donation ID into the database for later processing by `~se/web/scripts/process-pending-payments`. // This webhook receives POSTs when email from a Fractured Atlas donation is received at the SE Zoho email account. This script processes the email, and inserts the donation ID into the database for later processing by `~se/web/scripts/process-pending-payments`.
try{
$log = new Log(ZOHO_WEBHOOK_LOG_FILE_PATH); $log = new Log(ZOHO_WEBHOOK_LOG_FILE_PATH);
try{
HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]); HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]);
$log->Write('Received Zoho webhook.'); $log->Write('Received Zoho webhook.');
@ -17,7 +18,9 @@ try{
/** @var string $zohoWebhookSecret */ /** @var string $zohoWebhookSecret */
$zohoWebhookSecret = get_cfg_var('se.secrets.zoho.webhook_secret'); $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(); throw new Exceptions\InvalidCredentialsException();
} }