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

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.