diff --git a/config/phpstan/phpstan.neon b/config/phpstan/phpstan.neon index 5322d5df..3984efca 100644 --- a/config/phpstan/phpstan.neon +++ b/config/phpstan/phpstan.neon @@ -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 diff --git a/lib/Db.php b/lib/Db.php index db1ca7d2..4785b72f 100644 --- a/lib/Db.php +++ b/lib/Db.php @@ -9,21 +9,22 @@ class Db{ } /** - * @param string $query - * @param array $args - * @param string $class - * @return Array - */ + * @template T + * @param string $query + * @param array $args + * @param class-string $class + * @return Array + */ 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 $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 $args + */ public static function QueryInt(string $query, array $args = []): int{ $result = $GLOBALS['DbConnection']->Query($query, $args); diff --git a/lib/DbConnection.php b/lib/DbConnection.php index 49f95c2b..f11b6706 100644 --- a/lib/DbConnection.php +++ b/lib/DbConnection.php @@ -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 $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 - * @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 $params An array of parameters to bind to the SQL statement. + * @param class-string $class The type of object to return in the return array. + * @return Array + * @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 $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 - * @throws \PDOException When an error occurs during execution of the query. - */ + * @template T + * @param class-string $class + * @return Array + * @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 $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)); + } } diff --git a/lib/Ebook.php b/lib/Ebook.php index e40ff60c..fd494372 100644 --- a/lib/Ebook.php +++ b/lib/Ebook.php @@ -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 $TocEntries */ diff --git a/lib/HttpInput.php b/lib/HttpInput.php index 65527fa9..c701f1ed 100644 --- a/lib/HttpInput.php +++ b/lib/HttpInput.php @@ -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 */ public static function Array(HttpVariableSource $set, string $variable): ?array{ + /** @var array */ return self::GetHttpVar($variable, HttpVariableType::Array, $set); } + /** + * @return array|array|array|array|string|int|float|bool|null + */ private static function GetHttpVar(string $variable, HttpVariableType $type, HttpVariableSource $set): mixed{ $vars = []; diff --git a/lib/Library.php b/lib/Library.php index 36d2bd28..5bfa8e3e 100644 --- a/lib/Library.php +++ b/lib/Library.php @@ -113,6 +113,7 @@ class Library{ */ public static function GetEbooks(): array{ // Get all ebooks, unsorted. + /** @var array */ return self::GetFromApcu('ebooks'); } @@ -121,6 +122,7 @@ class Library{ * @throws Exceptions\AppException */ public static function GetEbooksByAuthor(string $wwwFilesystemPath): array{ + /** @var array */ return self::GetFromApcu('author-' . $wwwFilesystemPath); } @@ -129,6 +131,7 @@ class Library{ */ public static function GetEbooksByTag(string $tag): array{ try{ + /** @var array */ 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 */ 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 */ return self::GetFromApcu('collection-' . $collection); } @@ -158,6 +163,7 @@ class Library{ * @throws Exceptions\AppException */ public static function GetTags(): array{ + /** @var array */ return self::GetFromApcu('tags'); } @@ -538,7 +544,7 @@ class Library{ } /** - * @return array>> + * @return array>> * @throws Exceptions\AppException */ public static function RebuildBulkDownloadsCache(): array{ @@ -662,6 +668,7 @@ class Library{ return null; } + /** @var array $result */ $result = self::GetFromApcu('ebook-' . $ebookWwwFilesystemPath); if(sizeof($result) > 0){ diff --git a/lib/Template.php b/lib/Template.php index 2bb58312..ce02a135 100644 --- a/lib/Template.php +++ b/lib/Template.php @@ -25,7 +25,7 @@ class Template{ * @param array $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{ diff --git a/www/bulk-downloads/collection.php b/www/bulk-downloads/collection.php index 9749a34d..6c52ec58 100644 --- a/www/bulk-downloads/collection.php +++ b/www/bulk-downloads/collection.php @@ -16,10 +16,12 @@ if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){ $collection = []; try{ + /** @var array> $collection */ $collection = apcu_fetch('bulk-downloads-' . $class); } catch(Safe\Exceptions\ApcuException){ $result = Library::RebuildBulkDownloadsCache(); + /** @var array> $collection */ $collection = $result[$class]; } diff --git a/www/bulk-downloads/get.php b/www/bulk-downloads/get.php index fd08b626..7949fe04 100644 --- a/www/bulk-downloads/get.php +++ b/www/bulk-downloads/get.php @@ -17,10 +17,13 @@ try{ // Get all collections and then find the specific one we're looking for try{ + /** @var array $collections */ $collections = apcu_fetch('bulk-downloads-collections'); } catch(Safe\Exceptions\ApcuException){ $result = Library::RebuildBulkDownloadsCache(); + + /** @var array $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 $collections */ $collections = apcu_fetch('bulk-downloads-authors'); } catch(Safe\Exceptions\ApcuException){ $result = Library::RebuildBulkDownloadsCache(); + + /** @var array $collections */ $collections = $result['authors']; } @@ -70,7 +76,7 @@ catch(Exceptions\CollectionNotFoundException){ ?> 'Download ', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
-

Download the Label ?> Collection

+

Download the Label ?> Collection

Select the ebook format in which you’d like to download this collection.

You can also read about which ebook format to download.

diff --git a/www/ebooks/download.php b/www/ebooks/download.php index 7cce9bc3..e4b49a6b 100644 --- a/www/ebooks/download.php +++ b/www/ebooks/download.php @@ -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; diff --git a/www/ebooks/ebook.php b/www/ebooks/ebook.php index a1eab509..fbd922fb 100644 --- a/www/ebooks/ebook.php +++ b/www/ebooks/ebook.php @@ -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){

This ebook is thought to be free of copyright restrictions in the United States. It may still be under copyright in other countries. If you’re 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 you’re located in before accessing, downloading, or using it.

-
+
diff --git a/www/feeds/collection.php b/www/feeds/collection.php index 849ac269..23f9f3fe 100644 --- a/www/feeds/collection.php +++ b/www/feeds/collection.php @@ -23,9 +23,11 @@ if($type === 'atom'){ } try{ + /** @var array $feeds */ $feeds = apcu_fetch('feeds-index-' . $type . '-' . $class); } catch(Safe\Exceptions\ApcuException){ + /** @var array $feeds */ $feeds = Library::RebuildFeedsCache($type, $class); } ?> $ucType . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $ucType . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?> diff --git a/www/webhooks/github.php b/www/webhooks/github.php index 3e693675..e5a85fdf 100644 --- a/www/webhooks/github.php +++ b/www/webhooks/github.php @@ -35,6 +35,7 @@ try{ throw new Exceptions\WebhookException('Couldn\'t understand HTTP request.', $post); } + /** @var array> $data */ $data = json_decode($post, true); // Decide what event we just received. diff --git a/www/webhooks/postmark.php b/www/webhooks/postmark.php index ee33b917..5ee1a017 100644 --- a/www/webhooks/postmark.php +++ b/www/webhooks/postmark.php @@ -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.'); diff --git a/www/webhooks/zoho.php b/www/webhooks/zoho.php index 95af5015..0bbab671 100644 --- a/www/webhooks/zoho.php +++ b/www/webhooks/zoho.php @@ -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){