Enable strict exception type hint checking in PHPStan and add exception type hints

This commit is contained in:
Alex Cabal 2024-05-10 20:47:36 -05:00
parent 559e4a5e05
commit c4c8e7353f
26 changed files with 300 additions and 82 deletions

98
composer.lock generated
View file

@ -557,16 +557,16 @@
},
{
"name": "symfony/finder",
"version": "v6.4.0",
"version": "v6.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce"
"reference": "511c48990be17358c23bf45c5d71ab85d40fb764"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce",
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce",
"url": "https://api.github.com/repos/symfony/finder/zipball/511c48990be17358c23bf45c5d71ab85d40fb764",
"reference": "511c48990be17358c23bf45c5d71ab85d40fb764",
"shasum": ""
},
"require": {
@ -601,7 +601,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.0"
"source": "https://github.com/symfony/finder/tree/v6.4.7"
},
"funding": [
{
@ -617,20 +617,20 @@
"type": "tidelift"
}
],
"time": "2023-10-31T17:30:12+00:00"
"time": "2024-04-23T10:36:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@ -644,9 +644,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -683,7 +680,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@ -699,20 +696,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@ -726,9 +723,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -766,7 +760,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@ -782,20 +776,20 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": ""
},
"require": {
@ -803,9 +797,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -849,7 +840,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
},
"funding": [
{
@ -865,20 +856,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
"reference": "c565ad1e63f30e7477fc40738343c62b40bc672d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d",
"reference": "c565ad1e63f30e7477fc40738343c62b40bc672d",
"shasum": ""
},
"require": {
@ -886,9 +877,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -928,7 +916,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0"
},
"funding": [
{
@ -944,20 +932,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/process",
"version": "v6.4.2",
"version": "v6.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241"
"reference": "cdb1c81c145fd5aa9b0038bab694035020943381"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c4b1ef0bc80533d87a2e969806172f1c2a980241",
"reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241",
"url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381",
"reference": "cdb1c81c145fd5aa9b0038bab694035020943381",
"shasum": ""
},
"require": {
@ -989,7 +977,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.4.2"
"source": "https://github.com/symfony/process/tree/v6.4.7"
},
"funding": [
{
@ -1005,7 +993,7 @@
"type": "tidelift"
}
],
"time": "2023-12-22T16:42:54+00:00"
"time": "2024-04-18T09:22:46+00:00"
},
{
"name": "thecodingmachine/safe",
@ -1150,16 +1138,16 @@
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.10.56",
"version": "1.10.67",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "27816a01aea996191ee14d010f325434c0ee76fa"
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/27816a01aea996191ee14d010f325434c0ee76fa",
"reference": "27816a01aea996191ee14d010f325434c0ee76fa",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493",
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493",
"shasum": ""
},
"require": {
@ -1202,13 +1190,9 @@
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2024-01-15T10:43:00+00:00"
"time": "2024-04-16T07:22:02+00:00"
},
{
"name": "thecodingmachine/phpstan-safe-rule",

View file

@ -30,3 +30,14 @@ parameters:
- Emit404
- Emit403
- RedirectToLogin
exceptions:
check:
missingCheckedExceptionInThrows: true
tooWideThrowType: true
uncheckedExceptionRegexes:
-'#^Safe\\#'
uncheckedExceptionClasses:
- 'Exceptions\DatabaseQueryException'
- 'PDOException'
- 'TypeError'
- 'ValueError'

View file

@ -1,6 +1,6 @@
CREATE TABLE `PollVotes` (
`UserId` int(10) unsigned NOT NULL,
`PollItemId` int(10) unsigned NOT NULL,
`Created` datetime NOT NULL,
`Created` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
UNIQUE KEY `idxUnique` (`PollItemId`,`UserId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -89,6 +89,7 @@ class Artist{
* @throws Exceptions\InvalidArtistException
*/
public function Validate(): void{
/** @throws void */
$now = new DateTimeImmutable();
$thisYear = intval($now->format('Y'));
@ -136,6 +137,9 @@ class Artist{
return $result[0] ?? throw new Exceptions\ArtistNotFoundException();;
}
/**
* @throws Exceptions\InvalidArtistException
*/
public function Create(): void{
$this->Validate();
Db::Query('
@ -149,7 +153,7 @@ class Artist{
}
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\InvalidArtistException
*/
public static function GetOrCreate(Artist $artist): Artist{
$result = Db::Query('

View file

@ -1,5 +1,13 @@
<?
use Exceptions\InvalidImageUploadException;
use Exceptions\InvalidPageScanUrlException;
use Exceptions\InvalidUrlException;
use Safe\DateTimeImmutable;
use Safe\Exceptions\ExecException;
use Safe\Exceptions\FilesystemException;
use Safe\Exceptions\PcreException;
use function Safe\copy;
use function Safe\exec;
use function Safe\getimagesize;
@ -160,6 +168,9 @@ class Artwork{
return $this->_Tags;
}
/**
* @throws Exceptions\InvalidUrlException
*/
public function GetMuseum(): ?Museum{
if($this->_Museum === null){
try{
@ -252,6 +263,9 @@ class Artwork{
return $this->_Dimensions;
}
/**
* @throws Exceptions\AppException
*/
protected function GetEbook(): ?Ebook{
if($this->_Ebook === null){
if($this->EbookUrl === null){
@ -318,9 +332,10 @@ class Artwork{
}
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\InvalidArtworkException
*/
protected function Validate(?string $imagePath = null, bool $isImageRequired = true): void{
/** @throws void */
$now = new DateTimeImmutable();
$thisYear = intval($now->format('Y'));
$error = new Exceptions\InvalidArtworkException();
@ -509,6 +524,10 @@ class Artwork{
}
}
/**
* @throws Exceptions\InvalidUrlException
* @throws Exceptions\InvalidPageScanUrlException
*/
public static function NormalizePageScanUrl(string $url): string{
$outputUrl = $url;
@ -621,6 +640,9 @@ class Artwork{
return $outputUrl;
}
/**
* @throws Exceptions\InvalidImageUploadException
*/
private function WriteImageAndThumbnails(string $imagePath): void{
exec('exiftool -quiet -overwrite_original -all= ' . escapeshellarg($imagePath));
copy($imagePath, $this->ImageFsPath);
@ -645,6 +667,7 @@ class Artwork{
$this->Validate($imagePath, true);
/** @throws void */
$this->Created = new DateTimeImmutable();
$tags = [];
@ -700,7 +723,10 @@ class Artwork{
}
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\InvalidArtworkException
* @throws Exceptions\InvalidArtistException
* @throws Exceptions\InvalidArtworkTagException
* @throws Exceptions\InvalidImageUploadException
*/
public function Save(?string $imagePath = null): void{
$this->_UrlName = null;
@ -710,6 +736,7 @@ class Artwork{
// Manually set the updated timestamp, because if we only update the image and nothing else, the row's
// updated timestamp won't change automatically.
/** @throws void */
$this->Updated = new DateTimeImmutable();
$this->_ImageUrl = null;
$this->_ThumbUrl = null;

View file

@ -1,4 +1,7 @@
<?
use Exceptions\InvalidArtworkTagException;
use function Safe\preg_match;
use function Safe\preg_replace;
@ -20,7 +23,7 @@ class ArtworkTag extends Tag{
// *******
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\InvalidArtworkTagException
*/
public function Validate(): void{
$error = new Exceptions\InvalidArtworkTagException($this->Name);
@ -46,6 +49,9 @@ class ArtworkTag extends Tag{
}
}
/**
* @throws InvalidArtworkTagException
*/
public function Create(): void{
$this->Validate();
@ -57,7 +63,7 @@ class ArtworkTag extends Tag{
}
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\InvalidArtworkTagException
*/
public static function GetOrCreate(ArtworkTag $artworkTag): ArtworkTag{
$result = Db::Query('

View file

@ -39,6 +39,7 @@ class AtomFeed extends Feed{
// Did we actually update the feed? If so, write to file and update the index
if($this->HasChanged($this->Path)){
// Files don't match, save the file
/** @throws void */
$this->Updated = new DateTimeImmutable();
$this->Save();
return true;

View file

@ -188,6 +188,7 @@ class DbConnection{
switch($metadata[$i]['native_type'] ?? null){
case 'DATETIME':
case 'TIMESTAMP':
/** @throws void */
$object->{$metadata[$i]['name']} = new DateTimeImmutable($row[$i], new DateTimeZone('UTC'));
break;

View file

@ -1,5 +1,7 @@
<?
use Safe\DateTimeImmutable;
use function Safe\file_get_contents;
use function Safe\filesize;
use function Safe\json_encode;
@ -75,6 +77,11 @@ class Ebook{
public ?string $TextSinglePageSizeUnit = null;
public $TocEntries = null; // A list of non-Roman ToC entries ONLY IF the work has the 'se:is-a-collection' metadata element, null otherwise
/**
* @throws Exceptions\EbookNotFoundException
* @throws Exceptions\EbookParsingException
* @throws Exceptions\InvalidGitCommitException
*/
public function __construct(?string $wwwFilesystemPath = null){
if($wwwFilesystemPath === null){
return;
@ -88,7 +95,7 @@ class Ebook{
try{
$this->RepoFilesystemPath = preg_replace('/\.git$/ius', '', $this->RepoFilesystemPath);
}
catch(Exception){
catch(\Exception){
// We may get an exception from preg_replace if the passed repo wwwFilesystemPath contains invalid UTF-8 characters, whichis a common injection attack vector
throw new Exceptions\EbookNotFoundException('Invalid repo filesystem path: ' . $this->RepoFilesystemPath);
}
@ -128,11 +135,16 @@ class Ebook{
$bytes = @filesize($this->WwwFilesystemPath . '/text/single-page.xhtml');
$sizes = 'BKMGTP';
$factor = intval(floor((strlen((string)$bytes) - 1) / 3));
$this->TextSinglePageSizeNumber = sprintf('%.1f', $bytes / pow(1024, $factor));
try{
$this->TextSinglePageSizeNumber = sprintf('%.1f', $bytes / pow(1024, $factor));
}
catch(\DivisionByZeroError){
$this->TextSinglePageSizeNumber = '0';
}
$this->TextSinglePageSizeUnit = $sizes[$factor] ?? '';
$this->TextSinglePageUrl = $this->Url . '/text/single-page';
}
catch(Exception){
catch(\Exception){
// Single page file doesn't exist, just pass
}
@ -206,7 +218,13 @@ class Ebook{
}
// Now do some heavy XML lifting!
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $rawMetadata));
try{
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $rawMetadata));
}
catch(\Exception $ex){
throw new Exceptions\EbookParsingException($ex->getMessage());
}
$xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$this->Title = $this->NullIfEmpty($xml->xpath('/package/metadata/dc:title'));
@ -222,11 +240,13 @@ class Ebook{
$date = $xml->xpath('/package/metadata/dc:date') ?: [];
if($date !== false && sizeof($date) > 0){
/** @throws void */
$this->Created = new DateTimeImmutable((string)$date[0]);
}
$modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]') ?: [];
if($modifiedDate !== false && sizeof($modifiedDate) > 0){
/** @throws void */
$this->Updated = new DateTimeImmutable((string)$modifiedDate[0]);
}
@ -240,7 +260,12 @@ class Ebook{
// Fill the ToC if necessary
if($includeToc){
$this->TocEntries = [];
$tocDom = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($wwwFilesystemPath . '/toc.xhtml')));
try{
$tocDom = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($wwwFilesystemPath . '/toc.xhtml')));
}
catch(\Exception $ex){
throw new Exceptions\EbookParsingException($ex->getMessage());
}
$tocDom->registerXPathNamespace('epub', 'http://www.idpf.org/2007/ops');
foreach($tocDom->xpath('/html/body//nav[@epub:type="toc"]//a[not(contains(@epub:type, "z3998:roman")) and not(text() = "Titlepage" or text() = "Imprint" or text() = "Colophon" or text() = "Endnotes" or text() = "Uncopyright") and not(contains(@href, "halftitle"))]') ?: [] as $item){
$this->TocEntries[] = (string)$item;

View file

@ -0,0 +1,5 @@
<?
namespace Exceptions;
class InvalidGitCommitException extends AppException{
}

View file

@ -6,8 +6,16 @@ class GitCommit{
public string $Message;
public string $Hash;
/**
* @throws Exceptions\InvalidGitCommitException
*/
public function __construct(string $unixTimestamp, string $hash, string $message){
$this->Created = new DateTimeImmutable('@' . $unixTimestamp);
try{
$this->Created = new DateTimeImmutable('@' . $unixTimestamp);
}
catch(\Exception){
throw new Exceptions\InvalidGitCommitException('Invalid timestamp for Git commit.');
}
$this->Message = $message;
$this->Hash = $hash;
}

View file

@ -1,4 +1,8 @@
<?
use Safe\Exceptions\ImageException;
use Exceptions\InvalidImageUploadException;
use function Safe\exec;
use function Safe\imagecopyresampled;
use function Safe\imagecreatetruecolor;
@ -69,19 +73,37 @@ class Image{
return $handle;
}
/**
* @throws Exceptions\InvalidImageUploadException
*/
public function Resize(string $destImagePath, int $width, int $height): void{
$imageDimensions = getimagesize($this->Path);
try{
$imageDimensions = getimagesize($this->Path);
}
catch(\Safe\Exceptions\ImageException $ex){
throw new Exceptions\InvalidImageUploadException($ex->getMessage());
}
$imageWidth = $imageDimensions[0];
$imageHeight = $imageDimensions[1];
if($imageHeight > $imageWidth){
$destinationHeight = $height;
$destinationWidth = intval($destinationHeight * ($imageWidth / $imageHeight));
try{
$destinationWidth = intval($destinationHeight * ($imageWidth / $imageHeight));
}
catch(\DivisionByZeroError){
$destinationWidth = 0;
}
}
else{
$destinationWidth = $width;
$destinationHeight = intval($destinationWidth * ($imageHeight / $imageWidth));
try{
$destinationHeight = intval($destinationWidth * ($imageHeight / $imageWidth));
}
catch(\DivisionByZeroError){
$destinationHeight = 0;
}
}
$srcImageHandle = $this->GetImageHandle();

View file

@ -1,5 +1,15 @@
<?
use Exceptions\AppException;
use Exceptions\ArtistNotFoundException;
use Safe\DateTimeImmutable;
use Safe\Exceptions\ExecException;
use Safe\Exceptions\PcreException;
use Safe\Exceptions\FilesystemException;
use Safe\Exceptions\DatetimeException;
use Safe\Exceptions\ArrayException;
use Safe\Exceptions\MiscException;
use function Safe\apcu_fetch;
use function Safe\exec;
use function Safe\filemtime;
@ -18,6 +28,7 @@ class Library{
* @param array<string> $tags
* @param EbookSort $sort
* @return array<Ebook>
* @throws Exceptions\AppException
*/
public static function FilterEbooks(string $query = null, array $tags = [], EbookSort $sort = null){
$ebooks = Library::GetEbooks();
@ -109,6 +120,7 @@ class Library{
/**
* @return array<Ebook>
* @throws Exceptions\AppException
*/
public static function GetEbooks(): array{
// Get all ebooks, unsorted.
@ -117,6 +129,7 @@ class Library{
/**
* @return array<Ebook>
* @throws Exceptions\AppException
*/
public static function GetEbooksByAuthor(string $wwwFilesystemPath): array{
return self::GetFromApcu('author-' . $wwwFilesystemPath);
@ -136,6 +149,7 @@ class Library{
/**
* @return array<string, Collection>
* @throws Exceptions\AppException
*/
public static function GetEbookCollections(): array{
return self::GetFromApcu('collections');
@ -143,6 +157,7 @@ class Library{
/**
* @return array<Ebook>
* @throws Exceptions\AppException
*/
public static function GetEbooksByCollection(string $collection): array{
// Do we have the tag's ebooks cached?
@ -151,6 +166,7 @@ class Library{
/**
* @return array<Tag>
* @throws Exceptions\AppException
*/
public static function GetTags(): array{
return self::GetFromApcu('tags');
@ -303,8 +319,10 @@ class Library{
return ['artworks' => $artworks, 'artworksCount' => $artworksCount];
}
/**
* @return array<Artwork>
* @return array<Artwork>
* @throws Exceptions\ArtistNotFoundException
*/
public static function GetArtworksByArtist(?string $artistUrlName, ?string $status, ?int $submitterUserId): array{
if($artistUrlName === null){
@ -346,8 +364,10 @@ class Library{
return $artworks;
}
/**
* @return array<mixed>
* @throws Exceptions\AppException
*/
private static function GetFromApcu(string $variable): array{
$results = [];
@ -389,6 +409,7 @@ class Library{
/**
* @return array<Ebook>
* @throws Exceptions\AppException
*/
public static function Search(string $query): array{
$ebooks = Library::GetEbooks();
@ -432,6 +453,8 @@ class Library{
private static function FillBulkDownloadObject(string $dir, string $downloadType, string $urlRoot): stdClass{
$obj = new stdClass();
/** @throws void */
$now = new DateTimeImmutable();
// The count of ebooks in each file is stored as a filesystem attribute
@ -479,6 +502,7 @@ class Library{
$obj->ZipFiles[] = $zipFile;
}
/** @throws void */
$obj->Updated = new DateTimeImmutable('@' . filemtime($files[0]));
$obj->UpdatedString = $obj->Updated->format('M j');
// Add a period to the abbreviated month, but not if it's May (the only 3-letter month)
@ -526,6 +550,7 @@ class Library{
/**
* @return array<string, array<int|string, array<int|string, mixed>>>
* @throws Exceptions\AppException
*/
public static function RebuildBulkDownloadsCache(): array{
$collator = Collator::create('en_US'); // Used for sorting letters with diacritics like in author names
@ -546,7 +571,12 @@ class Library{
foreach($dirs as $dir){
$obj = self::FillBulkDownloadObject($dir, 'months', '/months');
$date = new DateTimeImmutable($obj->Label . '-01');
try{
$date = new DateTimeImmutable($obj->Label . '-01');
}
catch(\Exception){
throw new Exceptions\AppException('Couldn\'t parse date on bulk download object.');
}
$year = $date->format('Y');
$month = $date->format('F');
@ -588,6 +618,7 @@ class Library{
/**
* @return array<string, array<int|string, array<int|string, mixed>>>
* @throws Exceptions\AppException
*/
public static function RebuildFeedsCache(?string $returnType = null, ?string $returnClass = null): ?array{
$feedTypes = ['opds', 'atom', 'rss'];
@ -634,6 +665,9 @@ class Library{
return $retval;
}
/**
* @throws Exceptions\AppException
*/
public static function GetEbook(?string $ebookWwwFilesystemPath): ?Ebook{
if($ebookWwwFilesystemPath === null){
return null;
@ -649,6 +683,9 @@ class Library{
}
}
/**
* @throws Exceptions\AppException
*/
public static function RebuildCache(): void{
// We check a lockfile because this can be a long-running command.
// We don't want to queue up a bunch of these in case someone is refreshing the index constantly.

View file

@ -33,6 +33,7 @@ class Log{
return;
}
/** @throws void */
$now = new DateTimeImmutable();
fwrite($fp, $now->format('Y-m-d H:i:s') . "\t" . $this->RequestId . "\t" . $text . "\n");

View file

@ -1,4 +1,10 @@
<?
use Exceptions\InvalidUrlException;
use Safe\Exceptions\PcreException;
use Exceptions\InvalidMuseumUrlException;
use Exceptions\InvalidPageScanUrlException;
use function Safe\parse_url;
use function Safe\preg_match;
use function Safe\preg_replace;
@ -10,6 +16,11 @@ class Museum{
public string $Name;
public string $Domain;
/**
* @throws Exceptions\InvalidUrlException
* @throws Exceptions\InvalidMuseumUrlException
* @throws Exceptions\InvalidPageScanUrlException
*/
public static function NormalizeUrl(string $url): string{
$outputUrl = $url;

View file

@ -1,5 +1,9 @@
<?
use Exceptions\InvalidNewsletterSubscription;
use Exceptions\NewsletterSubscriptionExistsException;
use Safe\DateTimeImmutable;
use Safe\Exceptions\ErrorfuncException;
/**
* @property User $User
@ -33,6 +37,10 @@ class NewsletterSubscription{
// METHODS
// *******
/**
* @throws Exceptions\InvalidNewsletterSubscription
* @throws Exceptions\NewsletterSubscriptionExistsException
*/
public function Create(?string $expectedCaptcha = null, ?string $receivedCaptcha = null): void{
$this->Validate($expectedCaptcha, $receivedCaptcha);
@ -42,10 +50,18 @@ class NewsletterSubscription{
}
catch(Exceptions\UserNotFoundException){
// User doesn't exist, create the user
$this->User->Create();
try{
$this->User->Create();
}
catch(Exceptions\UserExistsException){
// User exists, pass
}
}
$this->UserId = $this->User->UserId;
/** @throws void */
$this->Created = new DateTimeImmutable();
try{
@ -66,6 +82,9 @@ class NewsletterSubscription{
$this->SendConfirmationEmail();
}
/**
* @throws Exceptions\InvalidNewsletterSubscription
*/
public function Save(): void{
$this->Validate();
@ -107,6 +126,10 @@ class NewsletterSubscription{
', [$this->UserId]);
}
/**
* @throws Exceptions\InvalidNewsletterSubscription
*/
public function Validate(?string $expectedCaptcha = null, ?string $receivedCaptcha = null): void{
$error = new Exceptions\InvalidNewsletterSubscription();

View file

@ -50,6 +50,7 @@ class OpdsFeed extends AtomFeed{
if($this->HasChanged($this->Path)){
// Files don't match, save the file and update the parent navigation feed with the last updated timestamp
/** @throws void */
$this->Updated = new DateTimeImmutable();
if($this->Parent !== null){

View file

@ -21,6 +21,7 @@ class Patron{
// *******
public function Create(): void{
/** @throws void */
$this->Created = new DateTimeImmutable();
Db::Query('
INSERT into Patrons (Created, UserId, IsAnonymous, AlternateName, IsSubscribedToEmails)

View file

@ -1,4 +1,7 @@
<?
use Exceptions\UserExistsException;
use Exceptions\PaymentExistsException;
use Safe\DateTimeImmutable;
/**
@ -23,6 +26,9 @@ class Payment{
// METHODS
// *******
/**
* @throws Exceptions\PaymentExistsException
*/
public function Create(): void{
if($this->UserId === null){
// Check if we have to create a new user in the database
@ -45,7 +51,13 @@ class Payment{
}
catch(Exceptions\UserNotFoundException){
// User doesn't exist, create it now
$this->User->Create();
try{
$this->User->Create();
}
catch(Exceptions\UserExistsException){
// User already exists, pass
}
}
$this->UserId = $this->User->UserId;

View file

@ -87,6 +87,7 @@ class Poll{
// *******
public function IsActive(): bool{
/** @throws void */
$now = new DateTimeImmutable();
if( ($this->Start !== null && $this->Start > $now) || ($this->End !== null && $this->End < $now)){
return false;

View file

@ -1,4 +1,7 @@
<?
use Exceptions\PollItemNotFoundException;
/**
* @property int $VoteCount
* @property Poll $Poll
@ -37,7 +40,7 @@ class PollItem{
// ***********
/**
* @throws Exceptions\PollNotFoundException
* @throws Exceptions\PollItemNotFoundException
*/
public static function Get(?int $pollItemId): PollItem{
if($pollItemId === null ){

View file

@ -1,4 +1,6 @@
<?
use Exceptions\InvalidPollVoteException;
use Safe\DateTimeImmutable;
/**
@ -86,6 +88,9 @@ class PollVote{
}
}
/**
* @throws Exceptions\InvalidPollVoteException
*/
public function Create(?string $email = null): void{
if($email !== null){
try{
@ -103,13 +108,11 @@ class PollVote{
}
$this->Validate();
$this->Created = new DateTimeImmutable();
Db::Query('
INSERT into PollVotes (UserId, PollItemId, Created)
INSERT into PollVotes (UserId, PollItemId)
values (?,
?,
?)
', [$this->UserId, $this->PollItemId, $this->Created]);
', [$this->UserId, $this->PollItemId]);
}
/**

View file

@ -1,5 +1,9 @@
<?
use Safe\DateTimeImmutable;
use Safe\Exceptions\DatetimeException;
use Safe\Exceptions\FilesystemException;
use Safe\Exceptions\ExecException;
use function Safe\file_get_contents;
use function Safe\filesize;
use function Safe\preg_replace;
@ -27,7 +31,9 @@ class RssFeed extends Feed{
protected function GetXmlString(): string{
if($this->XmlString === null){
$feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => (new DateTimeImmutable())->format('r')]);
/** @throws void */
$timestamp = (new DateTimeImmutable())->format('r');
$feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => $timestamp]);
$this->XmlString = $this->CleanXmlString($feed);
}

View file

@ -1,7 +1,10 @@
<?
use Exceptions\InvalidLoginException;
use Exceptions\PasswordRequiredException;
use Ramsey\Uuid\Uuid;
use Safe\DateTimeImmutable;
use Safe\Exceptions\DatetimeException;
use function Safe\strtotime;
/**
@ -35,6 +38,10 @@ class Session{
// METHODS
// *******
/**
* @throws Exceptions\InvalidLoginException
* @throws Exceptions\PasswordRequiredException
*/
public function Create(?string $email = null, ?string $password = null): void{
try{
$this->User = User::GetIfRegistered($email, $password);
@ -54,6 +61,8 @@ class Session{
else{
$uuid = Uuid::uuid4();
$this->SessionId = $uuid->toString();
/** @throws void */
$this->Created = new DateTimeImmutable();
Db::Query('
INSERT into Sessions (UserId, SessionId, Created)

View file

@ -1,4 +1,6 @@
<?
use Exceptions\UserExistsException;
use Ramsey\Uuid\Uuid;
use Safe\DateTimeImmutable;
@ -77,9 +79,14 @@ class User{
// METHODS
// *******
/**
* @throws UserExistsException
*/
public function Create(?string $password = null): void{
$uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString();
/** @throws void */
$this->Created = new DateTimeImmutable();
$this->PasswordHash = null;
@ -145,6 +152,7 @@ class User{
/**
* @throws Exceptions\UserNotFoundException
* @throws Exceptions\PasswordRequiredException
*/
public static function GetIfRegistered(?string $identifier, ?string $password = null): User{
// We consider a user "registered" if they have a row in the Benefits table.

View file

@ -2,6 +2,7 @@
use function Safe\preg_replace;
$page = HttpInput::Int(GET, 'page') ?? 1;
$pages = 0;
$perPage = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE;
$query = HttpInput::Str(GET, 'query') ?? '';
$tags = HttpInput::GetArray('tags') ?? [];
@ -9,6 +10,7 @@ $view = ViewType::tryFrom(HttpInput::Str(GET, 'view') ?? '');
$sort = EbookSort::tryFrom(HttpInput::Str(GET, 'sort') ?? '');
$queryString = '';
$queryStringParams = [];
$queryStringWithoutPage = '';
try{
if($page <= 0){
@ -96,6 +98,12 @@ catch(Exceptions\PageOutOfBoundsException){
header('Location: ' . $url);
exit();
}
catch(Exceptions\AppException $ex){
// Something very unexpected happened, log and emit 500
http_response_code(500); // Internal server error
Log::WriteErrorLogEntry($ex);
exit();
}
?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription, 'canonicalUrl' => $canonicalUrl]) ?>
<main class="ebooks">
<h1><?= $pageHeader ?></h1>