Bump PHPStan check level to max and add final round of type hints

This commit is contained in:
Alex Cabal 2024-05-13 10:48:05 -05:00
parent 110c091a7b
commit 70ae877dd8
15 changed files with 86 additions and 52 deletions

View file

@ -8,7 +8,7 @@ parameters:
# Ignore errors caused by Template static class reflection
- '#Call to an undefined static method Template::[a-zA-Z0-9\\_]+\(\)\.#'
level:
8
9
paths:
- %rootDir%/../../../lib
- %rootDir%/../../../www

View file

@ -9,21 +9,22 @@ class Db{
}
/**
* @param string $query
* @param array<mixed> $args
* @param string $class
* @return Array<mixed>
*/
* @template T
* @param string $query
* @param array<mixed> $args
* @param class-string<T> $class
* @return Array<T>
*/
public static function Query(string $query, array $args = [], string $class = 'stdClass'): array{
return $GLOBALS['DbConnection']->Query($query, $args, $class);
}
/**
* Returns a single integer value for the first column database query result.
* This is useful for queries that return a single integer as a result, like count(*) or sum(*).
* @param string $query
* @param array<mixed> $args
*/
* Returns a single integer value for the first column database query result.
* This is useful for queries that return a single integer as a result, like count(*) or sum(*).
* @param string $query
* @param array<mixed> $args
*/
public static function QueryInt(string $query, array $args = []): int{
$result = $GLOBALS['DbConnection']->Query($query, $args);

View file

@ -41,24 +41,26 @@ class DbConnection{
$connectionString .= ';dbname=' . $defaultDatabase;
}
// We can't use persistent connections (connection pooling) because we would have race condition problems with last_insert_id()
$params = [\PDO::ATTR_EMULATE_PREPARES => false, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_PERSISTENT => false];
if($forceUtf8){
$params[\PDO::MYSQL_ATTR_INIT_COMMAND] = 'set names utf8mb4 collate utf8mb4_unicode_ci;';
}
// We can't use persistent connections (connection pooling) because we would have race condition problems with last_insert_id()
$this->_link = new \PDO($connectionString, $user, $password, $params);
}
/**
* @param string $sql The SQL query to execute.
* @param array<mixed> $params An array of parameters to bind to the SQL statement.
* @param string $class The type of object to return in the return array.
* @return Array<mixed>
* @throws Exceptions\DuplicateDatabaseKeyException When a unique key constraint has been violated.
* @throws Exceptions\DatabaseQueryException When an error occurs during execution of the query.
*/
* Execute a generic query in the database.
* @template T
* @param string $sql The SQL query to execute.
* @param array<mixed> $params An array of parameters to bind to the SQL statement.
* @param class-string<T> $class The type of object to return in the return array.
* @return Array<T>
* @throws Exceptions\DuplicateDatabaseKeyException When a unique key constraint has been violated.
* @throws Exceptions\DatabaseQueryException When an error occurs during execution of the query.
*/
public function Query(string $sql, array $params = [], string $class = 'stdClass'): array{
if($this->_link === null){
return [];
@ -131,19 +133,11 @@ class DbConnection{
}
/**
* @param \PDOException $ex The exception to create details from.
* @param string $sql The prepared SQL that caused the exception.
* @param array<mixed> $params The parameters passed to the prepared SQL.
*/
private function CreateDetailedException(\PDOException $ex, string $sql, array $params): Exceptions\DatabaseQueryException{
// Throw a custom exception that includes more information on the query and paramaters
return new Exceptions\DatabaseQueryException('Error when executing query: ' . $ex->getMessage() . '. Query: ' . $sql . '. Parameters: ' . vds($params));
}
/**
* @return Array<mixed>
* @throws \PDOException When an error occurs during execution of the query.
*/
* @template T
* @param class-string<T> $class
* @return Array<T>
* @throws \PDOException When an error occurs during execution of the query.
*/
private function ExecuteQuery(\PDOStatement $handle, string $class = 'stdClass'): array{
$handle->execute();
@ -287,4 +281,14 @@ class DbConnection{
return $id;
}
/**
* @param \PDOException $ex The exception to create details from.
* @param string $sql The prepared SQL that caused the exception.
* @param array<mixed> $params The parameters passed to the prepared SQL.
*/
private function CreateDetailedException(\PDOException $ex, string $sql, array $params): Exceptions\DatabaseQueryException{
// Throw a custom exception that includes more information on the query and paramaters
return new Exceptions\DatabaseQueryException('Error when executing query: ' . $ex->getMessage() . '. Query: ' . $sql . '. Parameters: ' . vds($params));
}
}

View file

@ -15,7 +15,7 @@ class Ebook{
public string $WwwFilesystemPath;
public string $RepoFilesystemPath;
public string $Url;
public string $KindleCoverUrl;
public ?string $KindleCoverUrl;
public string $EpubUrl;
public string $AdvancedEpubUrl;
public string $KepubUrl;
@ -68,8 +68,8 @@ class Ebook{
public string $TitleWithCreditsHtml = '';
public DateTimeImmutable $Created;
public DateTimeImmutable $Updated;
public string $TextUrl;
public string $TextSinglePageUrl;
public ?string $TextUrl;
public ?string $TextSinglePageUrl;
public ?string $TextSinglePageSizeNumber = null;
public ?string $TextSinglePageSizeUnit = null;
/** @var ?array<string> $TocEntries */

View file

@ -84,18 +84,22 @@ class HttpInput{
return null;
}
/** @var ?string $var */
return $var;
}
public static function Int(HttpVariableSource $set, string $variable): ?int{
/** @var ?int */
return self::GetHttpVar($variable, HttpVariableType::Integer, $set);
}
public static function Bool(HttpVariableSource $set, string $variable): ?bool{
/** @var ?bool */
return self::GetHttpVar($variable, HttpVariableType::Boolean, $set);
}
public static function Dec(HttpVariableSource $set, string $variable): ?float{
/** @var ?float */
return self::GetHttpVar($variable, HttpVariableType::Decimal, $set);
}
@ -104,9 +108,13 @@ class HttpInput{
* @return array<string>
*/
public static function Array(HttpVariableSource $set, string $variable): ?array{
/** @var array<string> */
return self::GetHttpVar($variable, HttpVariableType::Array, $set);
}
/**
* @return array<string>|array<int>|array<float>|array<bool>|string|int|float|bool|null
*/
private static function GetHttpVar(string $variable, HttpVariableType $type, HttpVariableSource $set): mixed{
$vars = [];

View file

@ -113,6 +113,7 @@ class Library{
*/
public static function GetEbooks(): array{
// Get all ebooks, unsorted.
/** @var array<Ebook> */
return self::GetFromApcu('ebooks');
}
@ -121,6 +122,7 @@ class Library{
* @throws Exceptions\AppException
*/
public static function GetEbooksByAuthor(string $wwwFilesystemPath): array{
/** @var array<Ebook> */
return self::GetFromApcu('author-' . $wwwFilesystemPath);
}
@ -129,6 +131,7 @@ class Library{
*/
public static function GetEbooksByTag(string $tag): array{
try{
/** @var array<Ebook> */
return apcu_fetch('tag-' . $tag) ?? [];
}
catch(Safe\Exceptions\ApcuException){
@ -141,6 +144,7 @@ class Library{
* @throws Exceptions\AppException
*/
public static function GetEbookCollections(): array{
/** @var array<string, Collection> */
return self::GetFromApcu('collections');
}
@ -150,6 +154,7 @@ class Library{
*/
public static function GetEbooksByCollection(string $collection): array{
// Do we have the tag's ebooks cached?
/** @var array<Ebook> */
return self::GetFromApcu('collection-' . $collection);
}
@ -158,6 +163,7 @@ class Library{
* @throws Exceptions\AppException
*/
public static function GetTags(): array{
/** @var array<Tag> */
return self::GetFromApcu('tags');
}
@ -538,7 +544,7 @@ class Library{
}
/**
* @return array<string, array<int|string, array<int|string, mixed>>>
* @return array<string, array<int|string, array<int|string, stdClass>>>
* @throws Exceptions\AppException
*/
public static function RebuildBulkDownloadsCache(): array{
@ -662,6 +668,7 @@ class Library{
return null;
}
/** @var array<Ebook> $result */
$result = self::GetFromApcu('ebook-' . $ebookWwwFilesystemPath);
if(sizeof($result) > 0){

View file

@ -25,7 +25,7 @@ class Template{
* @param array<mixed> $arguments
*/
public static function __callStatic(string $function, array $arguments): string{
if(isset($arguments[0])){
if(isset($arguments[0]) && is_array($arguments[0])){
return self::Get($function, $arguments[0]);
}
else{

View file

@ -16,10 +16,12 @@ if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
$collection = [];
try{
/** @var array<string, array<string, stdClass>> $collection */
$collection = apcu_fetch('bulk-downloads-' . $class);
}
catch(Safe\Exceptions\ApcuException){
$result = Library::RebuildBulkDownloadsCache();
/** @var array<string, array<string, stdClass>> $collection */
$collection = $result[$class];
}

View file

@ -17,10 +17,13 @@ try{
// Get all collections and then find the specific one we're looking for
try{
/** @var array<stdClass> $collections */
$collections = apcu_fetch('bulk-downloads-collections');
}
catch(Safe\Exceptions\ApcuException){
$result = Library::RebuildBulkDownloadsCache();
/** @var array<stdClass> $collections */
$collections = $result['collections'];
}
@ -41,10 +44,13 @@ try{
// Get all authors and then find the specific one we're looking for
try{
/** @var array<stdClass> $collections */
$collections = apcu_fetch('bulk-downloads-authors');
}
catch(Safe\Exceptions\ApcuException){
$result = Library::RebuildBulkDownloadsCache();
/** @var array<stdClass> $collections */
$collections = $result['authors'];
}
@ -70,7 +76,7 @@ catch(Exceptions\CollectionNotFoundException){
?><?= Template::Header(['title' => 'Download ', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
<main>
<section class="bulk-downloads">
<h1>Download the <?= $collection->Label ?> Collection</h1>
<h1>Download the <?= $collection?->Label ?> Collection</h1>
<? if($canDownload){ ?>
<p>Select the ebook format in which youd like to download this collection.</p>
<p>You can also read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which ebook format to download</a>.</p>

View file

@ -16,16 +16,13 @@ try{
// Do we have the ebook cached?
try{
/** @var Ebook $ebook */
$ebook = apcu_fetch('ebook-' . $wwwFilesystemPath);
}
catch(Safe\Exceptions\ApcuException){
$ebook = new Ebook($wwwFilesystemPath);
}
if($ebook === null){
throw new Exceptions\InvalidFileException();
}
switch($format){
case EbookFormat::Kepub:
$downloadUrl = $ebook->KepubUrl;

View file

@ -28,6 +28,7 @@ try{
// https://standardebooks.org/ebooks/omar-khayyam/the-rubaiyat-of-omar-khayyam/edward-fitzgerald/edmund-dulac
// We can tell because if so, the dir we are passed will exist, but there will be no 'src' folder.
if(is_dir($wwwFilesystemPath) && !is_dir($wwwFilesystemPath . '/src')){
/** @var DirectoryIterator $file */
foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($wwwFilesystemPath)) as $file){
// This iterator will do a deep scan on the directory. When we hit another directory, the filename will be "." and the path will contain the directory path.
// We want to find where the "src" directory is, and the directory directly below that will be the final web URL we're looking for.
@ -39,6 +40,7 @@ try{
// Do we have the ebook cached?
try{
/** @var Ebook $ebook */
$ebook = apcu_fetch('ebook-' . $wwwFilesystemPath);
}
catch(Safe\Exceptions\ApcuException){
@ -211,7 +213,7 @@ catch(Exceptions\EbookNotFoundException){
<p class="us-pd-warning">This ebook is thought to be free of copyright restrictions in the United States. It may still be under copyright in other countries. If youre not located in the United States, you must check your local laws to verify that this ebook is free of copyright restrictions in the country youre located in before accessing, downloading, or using it.</p>
<div class="downloads-container">
<figure class="<? if($ebook->WordCount < 100000){ ?>small<? }elseif($ebook->WordCount >= 100000 && $ebook->WordCount < 200000){ ?>medium<? }elseif($ebook->WordCount >= 200000 && $ebook->WordCount <= 300000){ ?>large<? }elseif($ebook->WordCount >= 300000 && $ebook->WordCount < 400000){ ?>xlarge<? }elseif($ebook->WordCount >= 400000){ ?>xxlarge<? } ?>">
<figure class="<? if($ebook->WordCount < 100000){ ?>small<? }elseif($ebook->WordCount < 200000){ ?>medium<? }elseif($ebook->WordCount <= 300000){ ?>large<? }elseif($ebook->WordCount < 400000){ ?>xlarge<? }else{ ?>xxlarge<? } ?>">
<picture>
<source srcset="<?= $ebook->CoverImage2xAvifUrl ?> 2x, <?= $ebook->CoverImageAvifUrl ?> 1x" type="image/avif"/>
<source srcset="<?= $ebook->CoverImage2xUrl ?> 2x, <?= $ebook->CoverImageUrl ?> 1x" type="image/jpg"/>

View file

@ -23,9 +23,11 @@ if($type === 'atom'){
}
try{
/** @var array<stdClass> $feeds */
$feeds = apcu_fetch('feeds-index-' . $type . '-' . $class);
}
catch(Safe\Exceptions\ApcuException){
/** @var array<stdClass> $feeds */
$feeds = Library::RebuildFeedsCache($type, $class);
}
?><?= Template::Header(['title' => $ucType . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $ucType . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?>

View file

@ -35,6 +35,7 @@ try{
throw new Exceptions\WebhookException('Couldn\'t understand HTTP request.', $post);
}
/** @var array<array<string>> $data */
$data = json_decode($post, true);
// Decide what event we just received.

View file

@ -21,13 +21,16 @@ try{
throw new Exceptions\InvalidCredentialsException();
}
$post = json_decode(file_get_contents('php://input'));
$post = file_get_contents('php://input');
if(!$post || !property_exists($post, 'RecordType')){
/** @var stdClass $data */
$data = json_decode($post);
if(!property_exists($data, 'RecordType')){
throw new Exceptions\WebhookException('Couldn\'t understand HTTP request.', $post);
}
if($post->RecordType == 'SpamComplaint'){
if($data->RecordType == 'SpamComplaint'){
// Received when a user marks an email as spam
$log->Write('Event type: spam complaint.');
@ -36,13 +39,13 @@ try{
from NewsletterSubscriptions ns
inner join Users u using(UserId)
where u.Email = ?
', [$post->Email]);
', [$data->Email]);
}
elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressSending){
elseif($data->RecordType == 'SubscriptionChange' && $data->SuppressSending){
// Received when a user clicks Postmark's "Unsubscribe" link in a newsletter email
$log->Write('Event type: unsubscribe.');
$email = $post->Recipient;
$email = $data->Recipient;
// Remove the email from our newsletter list
Db::Query('
@ -54,18 +57,18 @@ try{
// Remove the suppression from Postmark, since we deleted it from our own list we will never email them again anyway
$handle = curl_init();
curl_setopt($handle, CURLOPT_URL, 'https://api.postmarkapp.com/message-streams/' . $post->MessageStream . '/suppressions/delete');
curl_setopt($handle, CURLOPT_URL, 'https://api.postmarkapp.com/message-streams/' . $data->MessageStream . '/suppressions/delete');
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($handle, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Accept: application/json', 'X-Postmark-Server-Token: ' . $smtpUsername]);
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($handle, CURLOPT_POSTFIELDS, '{"Suppressions": [{"EmailAddress": "' . $email . '"}]}');
curl_exec($handle);
}
elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressionReason === null){
elseif($data->RecordType == 'SubscriptionChange' && $data->SuppressionReason === null){
$log->Write('Event type: suppression deletion.');
}
else{
$log->Write('Unrecognized event: ' . $post->RecordType);
$log->Write('Unrecognized event: ' . $data->RecordType);
}
$log->Write('Event processed.');

View file

@ -23,6 +23,7 @@ try{
throw new Exceptions\InvalidCredentialsException();
}
/** @var stdClass $data */
$data = json_decode($post);
if($data->fromAddress == 'support@fracturedatlas.org' && strpos($data->subject, 'NOTICE:') !== false){