From 124e8343fc5195d04c9d56dfb1ffd7bb663e5992 Mon Sep 17 00:00:00 2001 From: Alex Cabal Date: Tue, 4 Mar 2025 16:08:55 -0600 Subject: [PATCH] Completely type hint template functions and switch to named arguments --- config/phpstan/phpstan.neon | 4 - lib/AtomFeed.php | 4 +- lib/NewsletterSubscription.php | 4 +- lib/OpdsAcquisitionFeed.php | 8 +- lib/OpdsFeed.php | 3 + lib/OpdsNavigationFeed.php | 5 +- lib/Patron.php | 12 +- lib/Project.php | 12 +- lib/RssFeed.php | 5 +- lib/Template.php | 117 +++++++++--------- lib/TemplateBase.php | 80 ++++++++++++ scripts/ingest-fa-payments | 4 +- scripts/process-pending-payments | 8 +- templates/ArtworkForm.php | 9 +- templates/ArtworkList.php | 50 ++++---- templates/ArtworkStatus.php | 16 --- templates/AtomFeed.php | 4 +- templates/CollectionDescriptor.php | 11 +- templates/DonationCounter.php | 4 +- templates/DonationProgress.php | 4 +- templates/EbookCarousel.php | 2 +- templates/EbookGrid.php | 6 +- templates/EbookMetadata.php | 2 +- templates/EbookPlaceholderForm.php | 11 +- templates/EmailFooter.php | 2 +- templates/EmailHeader.php | 16 ++- templates/EmailManagerNewProject.php | 6 +- templates/EmailPatronsCircleCompleted.php | 2 +- .../EmailPatronsCircleRecurringCompleted.php | 2 +- templates/Header.php | 48 ++++--- templates/OpdsAcquisitionFeed.php | 8 +- templates/OpdsNavigationFeed.php | 2 +- templates/ProjectDetailsTable.php | 10 +- templates/ProjectForm.php | 10 +- templates/ProjectsTable.php | 6 +- templates/RssFeed.php | 2 +- templates/SearchForm.php | 5 +- templates/UserForm.php | 11 +- templates/WantedEbooksList.php | 5 +- www/403.php | 2 +- www/404.php | 2 +- www/451.php | 2 +- www/about/accessibility.php | 2 +- www/about/index.php | 2 +- www/about/our-goals.php | 2 +- .../standard-ebooks-and-the-public-domain.php | 2 +- .../what-makes-standard-ebooks-different.php | 2 +- www/artists/get.php | 4 +- www/artworks/edit.php | 24 ++-- www/artworks/get.php | 18 ++- www/artworks/index.php | 4 +- www/artworks/new.php | 25 ++-- www/authors/get.php | 4 +- www/blog/death-and-beauty-in-the-alps.php | 4 +- .../edith-whartons-vision-of-literary-art.php | 4 +- www/blog/index.php | 2 +- .../joyces-ulysses-the-rubaiyat-and-yes.php | 4 +- www/blog/public-domain-day-2025.php | 4 +- www/bulk-downloads/collection.php | 7 +- www/bulk-downloads/download.php | 2 +- www/bulk-downloads/get.php | 25 ++-- www/bulk-downloads/index.php | 2 +- www/collections/get.php | 4 +- www/collections/index.php | 2 +- .../a-basic-standard-ebooks-source-folder.php | 2 +- www/contribute/collections-policy.php | 2 +- ...s-when-working-on-public-domain-ebooks.php | 2 +- ...how-to-choose-and-create-a-cover-image.php | 2 +- ...ow-to-conquer-complex-drama-formatting.php | 2 +- ...how-to-create-figures-for-music-scores.php | 2 +- ...ate-svgs-from-maps-with-several-colors.php | 2 +- ...ew-an-ebook-production-for-publication.php | 2 +- ...ure-and-style-large-poetic-productions.php | 2 +- www/contribute/how-tos/index.php | 2 +- ...ings-to-look-out-for-when-proofreading.php | 2 +- www/contribute/index.php | 2 +- www/contribute/producers.php | 2 +- .../producing-an-ebook-step-by-step.php | 2 +- www/contribute/report-errors-upstream.php | 2 +- www/contribute/report-errors.php | 2 +- www/contribute/spreadsheets.php | 2 +- .../tips-for-editors-and-proofreaders.php | 2 +- .../uncategorized-art-resources.php | 2 +- www/contribute/wanted-ebooks.php | 8 +- www/donate/index.php | 6 +- www/ebook-placeholders/delete.php | 11 +- www/ebook-placeholders/edit.php | 13 +- www/ebook-placeholders/get.php | 19 ++- www/ebook-placeholders/new.php | 32 +++-- www/ebooks/download.php | 2 +- www/ebooks/get.php | 10 +- www/ebooks/index.php | 23 ++-- www/feeds/401.php | 2 +- www/feeds/atom/index.php | 2 +- www/feeds/atom/search.php | 6 +- www/feeds/atom/style.php | 2 +- www/feeds/collection.php | 2 +- www/feeds/get.php | 2 +- www/feeds/index.php | 2 +- www/feeds/opds/search.php | 2 +- www/feeds/opds/style.php | 2 +- www/feeds/rss/index.php | 2 +- www/feeds/rss/search.php | 2 +- www/feeds/rss/style.php | 2 +- www/help/how-to-use-our-ebooks.php | 2 +- www/help/index.php | 2 +- www/help/managing-your-recurring-donation.php | 2 +- www/index.php | 2 +- www/newsletter/subscriptions/delete.php | 2 +- www/newsletter/subscriptions/get.php | 2 +- www/newsletter/subscriptions/new.php | 4 +- www/newsletter/subscriptions/success.php | 2 +- www/polls/get.php | 2 +- www/polls/index.php | 2 +- www/polls/votes/get.php | 2 +- www/polls/votes/index.php | 2 +- www/polls/votes/new.php | 4 +- www/projects/edit.php | 13 +- www/projects/index.php | 14 +-- www/projects/new.php | 16 +-- www/sessions/new.php | 4 +- www/settings/index.php | 2 +- www/users/edit.php | 26 ++-- www/users/get.php | 8 +- www/users/projects/index.php | 14 +-- 125 files changed, 542 insertions(+), 450 deletions(-) create mode 100644 lib/TemplateBase.php delete mode 100644 templates/ArtworkStatus.php diff --git a/config/phpstan/phpstan.neon b/config/phpstan/phpstan.neon index 2c10994b..a6826fd0 100644 --- a/config/phpstan/phpstan.neon +++ b/config/phpstan/phpstan.neon @@ -9,10 +9,6 @@ parameters: checkInternalClassCaseSensitivity: true checkTooWideReturnTypesInProtectedAndPublicMethods: true - ignoreErrors: - # Ignore errors caused by `Template` static class reflection. - - '#Call to an undefined static method Template::[a-zA-Z0-9\\_]+\(\)\.#' - bootstrapFiles: - %rootDir%/../../../lib/Constants.php diff --git a/lib/AtomFeed.php b/lib/AtomFeed.php index 69318480..f0cd14f7 100644 --- a/lib/AtomFeed.php +++ b/lib/AtomFeed.php @@ -26,7 +26,9 @@ class AtomFeed extends Feed{ protected function GetXmlString(): string{ if(!isset($this->_XmlString)){ - $feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'subtitle' => $this->Subtitle, 'updated' => $this->Updated, 'entries' => $this->Entries]); + /** @var array $entries */ + $entries = $this->Entries; + $feed = Template::AtomFeed(id: $this->Id, url: $this->Url, title: $this->Title, subtitle: $this->Subtitle, updated: $this->Updated, entries: $entries); $this->_XmlString = $this->CleanXmlString($feed); } diff --git a/lib/NewsletterSubscription.php b/lib/NewsletterSubscription.php index 9b4f9041..ee9ddd0b 100644 --- a/lib/NewsletterSubscription.php +++ b/lib/NewsletterSubscription.php @@ -98,8 +98,8 @@ class NewsletterSubscription{ $em->ToName = $this->User->Name; } $em->Subject = 'Action required: confirm your newsletter subscription'; - $em->Body = Template::EmailNewsletterConfirmation(['subscription' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]); - $em->TextBody = Template::EmailNewsletterConfirmationText(['subscription' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]); + $em->Body = Template::EmailNewsletterConfirmation(subscription: $this, isSubscribedToSummary: $this->IsSubscribedToSummary, isSubscribedToNewsletter: $this->IsSubscribedToNewsletter); + $em->TextBody = Template::EmailNewsletterConfirmationText(subscription: $this, isSubscribedToSummary: $this->IsSubscribedToSummary, isSubscribedToNewsletter: $this->IsSubscribedToNewsletter); $em->Send(); } diff --git a/lib/OpdsAcquisitionFeed.php b/lib/OpdsAcquisitionFeed.php index fc38ba65..d28a689b 100644 --- a/lib/OpdsAcquisitionFeed.php +++ b/lib/OpdsAcquisitionFeed.php @@ -1,7 +1,13 @@ $Entries + */ class OpdsAcquisitionFeed extends OpdsFeed{ public bool $IsCrawlable; + /** + * @param array $entries + */ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent, bool $isCrawlable = false){ parent::__construct($title, $subtitle, $url, $path, $entries, $parent); $this->IsCrawlable = $isCrawlable; @@ -13,6 +19,6 @@ class OpdsAcquisitionFeed extends OpdsFeed{ // ******* protected function GetXmlString(): string{ - return $this->_XmlString ??= $this->CleanXmlString(Template::OpdsAcquisitionFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updated' => $this->Updated, 'isCrawlable' => $this->IsCrawlable, 'subtitle' => $this->Subtitle, 'entries' => $this->Entries])); + return $this->_XmlString ??= $this->CleanXmlString(Template::OpdsAcquisitionFeed(id: $this->Id, url: $this->Url, title: $this->Title, parentUrl: $this->Parent->Url ?? '', updated: $this->Updated, isCrawlable: $this->IsCrawlable, subtitle: $this->Subtitle, entries: $this->Entries)); } } diff --git a/lib/OpdsFeed.php b/lib/OpdsFeed.php index d400122e..e096e5c5 100644 --- a/lib/OpdsFeed.php +++ b/lib/OpdsFeed.php @@ -2,6 +2,9 @@ use Safe\DateTimeImmutable; use function Safe\file_put_contents; +/** + * @property array $Entries + */ abstract class OpdsFeed extends AtomFeed{ public ?OpdsNavigationFeed $Parent = null; diff --git a/lib/OpdsNavigationFeed.php b/lib/OpdsNavigationFeed.php index 5287e2fb..d726e854 100644 --- a/lib/OpdsNavigationFeed.php +++ b/lib/OpdsNavigationFeed.php @@ -2,6 +2,9 @@ use Safe\DateTimeImmutable; use function Safe\file_get_contents; +/** + * @property array $Entries + */ class OpdsNavigationFeed extends OpdsFeed{ /** * @param array $entries @@ -38,6 +41,6 @@ class OpdsNavigationFeed extends OpdsFeed{ // ******* protected function GetXmlString(): string{ - return $this->_XmlString ??= $this->CleanXmlString(Template::OpdsNavigationFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updated' => $this->Updated, 'subtitle' => $this->Subtitle, 'entries' => $this->Entries])); + return $this->_XmlString ??= $this->CleanXmlString(Template::OpdsNavigationFeed(id: $this->Id, url: $this->Url, title: $this->Title, parentUrl: $this->Parent->Url ?? '', updated: $this->Updated, subtitle: $this->Subtitle, entries: $this->Entries)); } } diff --git a/lib/Patron.php b/lib/Patron.php index 4eaa31f8..27d3007c 100644 --- a/lib/Patron.php +++ b/lib/Patron.php @@ -83,8 +83,8 @@ class Patron{ $em->From = EDITOR_IN_CHIEF_EMAIL_ADDRESS; $em->FromName = EDITOR_IN_CHIEF_NAME; $em->Subject = 'Thank you for supporting Standard Ebooks!'; - $em->Body = Template::EmailPatronsCircleWelcome(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]); - $em->TextBody = Template::EmailPatronsCircleWelcomeText(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]); + $em->Body = Template::EmailPatronsCircleWelcome(isAnonymous: $this->IsAnonymous, isReturning: $isReturning); + $em->TextBody = Template::EmailPatronsCircleWelcomeText(isAnonymous: $this->IsAnonymous, isReturning: $isReturning); $em->Send(); if(!$isReturning){ @@ -92,8 +92,8 @@ class Patron{ $em->To = ADMIN_EMAIL_ADDRESS; $em->From = ADMIN_EMAIL_ADDRESS; $em->Subject = 'New Patrons Circle member'; - $em->Body = Template::EmailAdminNewPatron(['patron' => $this, 'payment' => $this->User->Payments[0]]); - $em->TextBody = Template::EmailAdminNewPatronText(['patron' => $this, 'payment' => $this->User->Payments[0]]);; + $em->Body = Template::EmailAdminNewPatron(patron: $this, payment: $this->User->Payments[0]); + $em->TextBody = Template::EmailAdminNewPatronText(patron: $this, payment: $this->User->Payments[0]);; $em->Send(); } } @@ -134,8 +134,8 @@ class Patron{ } else{ // Email one time donors who have expired after one year. - $em->Body = Template::EmailPatronsCircleCompleted(['ebooksThisYear' => $ebooksThisYear]); - $em->TextBody = Template::EmailPatronsCircleCompletedText(['ebooksThisYear' => $ebooksThisYear]); + $em->Body = Template::EmailPatronsCircleCompleted(ebooksThisYear: $ebooksThisYear); + $em->TextBody = Template::EmailPatronsCircleCompletedText(ebooksThisYear: $ebooksThisYear); } $em->Send(); diff --git a/lib/Project.php b/lib/Project.php index 8af0719d..94c16d06 100644 --- a/lib/Project.php +++ b/lib/Project.php @@ -397,8 +397,8 @@ final class Project{ $em->From = ADMIN_EMAIL_ADDRESS; $em->To = $this->Manager->Email; $em->Subject = 'New ebook project to manage and review'; - $em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'manage and review', 'user' => $this->Manager]); - $em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'manage and review', 'user' => $this->Manager]); + $em->Body = Template::EmailManagerNewProject(project: $this, role: 'manage and review', user: $this->Manager); + $em->TextBody = Template::EmailManagerNewProjectText(project: $this, role: 'manage and review', user: $this->Manager); $em->Send(); } } @@ -409,8 +409,8 @@ final class Project{ $em->From = ADMIN_EMAIL_ADDRESS; $em->To = $this->Manager->Email; $em->Subject = 'New ebook project to manage'; - $em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'manage', 'user' => $this->Manager]); - $em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'manage', 'user' => $this->Manager]); + $em->Body = Template::EmailManagerNewProject(project: $this, role: 'manage', user: $this->Manager); + $em->TextBody = Template::EmailManagerNewProjectText(project: $this, role: 'manage', user: $this->Manager); $em->Send(); } @@ -420,8 +420,8 @@ final class Project{ $em->From = ADMIN_EMAIL_ADDRESS; $em->To = $this->Reviewer->Email; $em->Subject = 'New ebook project to review'; - $em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'review', 'user' => $this->Reviewer]); - $em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'review', 'user' => $this->Reviewer]); + $em->Body = Template::EmailManagerNewProject(project: $this, role: 'review', user: $this->Reviewer); + $em->TextBody = Template::EmailManagerNewProjectText(project: $this, role: 'review', user: $this->Reviewer); $em->Send(); } } diff --git a/lib/RssFeed.php b/lib/RssFeed.php index e4420487..315b2201 100644 --- a/lib/RssFeed.php +++ b/lib/RssFeed.php @@ -3,6 +3,9 @@ use function Safe\file_get_contents; use function Safe\filesize; use function Safe\preg_replace; +/** + * @property array $Entries + */ class RssFeed extends Feed{ public string $Description; @@ -21,7 +24,7 @@ class RssFeed extends Feed{ // ******* protected function GetXmlString(): string{ - return $this->_XmlString ??= $this->CleanXmlString(Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => NOW])); + return $this->_XmlString ??= $this->CleanXmlString(Template::RssFeed(url: $this->Url, description: $this->Description, title: $this->Title, entries: $this->Entries, updated: NOW)); } public function SaveIfChanged(): bool{ diff --git a/lib/Template.php b/lib/Template.php index 5f80998d..7ae2b82c 100644 --- a/lib/Template.php +++ b/lib/Template.php @@ -1,62 +1,65 @@ $arguments - */ - protected static function Get(string $templateName, array $arguments = []): string{ - // Expand the passed variables to make them available to the included template. - // We use these funny names so that we can use 'name' and 'value' as template variables if we want to. - foreach($arguments as $innerName => $innerValue){ - $$innerName = $innerValue; - } - - ob_start(); - include(TEMPLATES_PATH . '/' . $templateName . '.php'); - $contents = ob_get_contents() ?: ''; - ob_end_clean(); - - return $contents; - } - - /** - * @param array $arguments - */ - public static function __callStatic(string $function, array $arguments): string{ - if(isset($arguments[0]) && is_array($arguments[0])){ - return self::Get($function, $arguments[0]); - } - else{ - return self::Get($function, $arguments); - } - } - - /** - * Exit the script while outputting the given HTTP code. - * - * @param bool $showPage If **`TRUE`**, show a special page given the HTTP code (like a 404 page). - * - * @return never - */ - public static function ExitWithCode(Enums\HttpCode $httpCode, bool $showPage = true, Enums\HttpRequestType $requestType = Enums\HttpRequestType::Web): void{ - http_response_code($httpCode->value); - - if($requestType == Enums\HttpRequestType::Web && $showPage){ - switch($httpCode){ - case Enums\HttpCode::Forbidden: - include(WEB_ROOT . '/403.php'); - break; - case Enums\HttpCode::NotFound: - include(WEB_ROOT . '/404.php'); - break; - } - } - - exit(); - } +use Safe\DateTimeImmutable; +/** + * @method static string ArtworkForm(Artwork $artwork, $isEditForm = false) + * @method static string ArtworkList(array $artworks) + * @method static string AtomFeed(string $id, string $url, string $title, ?string $subtitle = null, DateTimeImmutable $updated, array $entries) + * @method static string AtomFeedEntry(Ebook $entry) + * @method static string BulkDownloadTable(string $label, array $collections) + * @method static string CollectionDescriptor(?CollectionMembership $collectionMembership) + * @method static string ContributeAlert() + * @method static string DonationAlert() + * @method static string DonationCounter(bool $autoHide = true, bool $showDonateButton = true) + * @method static string DonationProgress(bool $autoHide = true, bool $showDonateButton = true) + * @method static string EbookCarousel(array $carousel, bool $isMultiSize = false) + * @method static string EbookGrid(array $ebooks, ?Collection $collection = null, Enums\ViewType $view = Enums\ViewType::Grid) + * @method static string EbookMetadata(Ebook $ebook, bool $showPlaceholderMetadata = false) + * @method static string EbookPlaceholderForm(Ebook $ebook, bool $isEditForm = false, bool $showProjectForm = true) + * @method static string EmailAdminNewPatron(Patron $patron, Payment $payment) + * @method static string EmailAdminNewPatronText(Patron $patron, Payment $payment) + * @method static string EmailAdminUnprocessedDonations() + * @method static string EmailAdminUnprocessedDonationsText() + * @method static string EmailDonationProcessingFailed(string $exception) + * @method static string EmailDonationProcessingFailedText(string $exception) + * @method static string EmailDonationThankYou() + * @method static string EmailDonationThankYouText() + * @method static string EmailFooter(bool $includeLinks = true) + * @method static string EmailFooterText() + * @method static string EmailHeader(?string $preheader = null, bool $hasLetterhead = false, bool $hasAdminTable = false) + * @method static string EmailManagerNewProject(Project $project, string $role, User $user) + * @method static string EmailManagerNewProjectText(Project $project, string $role, User $user) + * @method static string EmailNewsletterConfirmation(bool $isSubscribedToNewsletter, bool $isSubscribedToSummary, NewsletterSubscription $subscription) + * @method static string EmailNewsletterConfirmationText(bool $isSubscribedToNewsletter, bool $isSubscribedToSummary, NewsletterSubscription $subscription) + * @method static string EmailPatronsCircleCompleted(int $ebooksThisYear) + * @method static string EmailPatronsCircleCompletedText(int $ebooksThisYear) + * @method static string EmailPatronsCircleRecurringCompleted() + * @method static string EmailPatronsCircleRecurringCompletedText() + * @method static string EmailPatronsCircleWelcome(bool $isAnonymous, bool $isReturning) + * @method static string EmailPatronsCircleWelcomeText(bool $isAnonymous, bool $isReturning) + * @method static string EmailProjectAbandoned() + * @method static string EmailProjectAbandonedText() + * @method static string EmailProjectStalled() + * @method static string EmailProjectStalledText() + * @method static string Error(?Exception $exception) + * @method static string FeedHowTo() + * @method static string Footer() + * @method static string Header(?string $title = null, ?string $highlight = null, ?string $description = null, bool $isManual = false, bool $isXslt = false, ?string $feedUrl = null, ?string $feedTitle = null, bool $isErrorPage = false, ?string $downloadUrl = null, ?string $canonicalUrl = null, ?string $coverUrl = null, string $ogType = 'website', array $css = []) + * @method static string ImageCopyrightNotice() + * @method static string OpdsAcquisitionEntry(Ebook $entry) + * @method static string OpdsAcquisitionFeed(string $id, string $url, string $parentUrl, string $title, ?string $subtitle, DateTimeImmutable $updated, array $entries, bool $isCrawlable = false) + * @method static string OpdsNavigationFeed(string $id, string $url, ?string $parentUrl, string $title, ?string $subtitle, DateTimeImmutable $updated, array $entries) + * @method static string ProjectDetailsTable(Project $project, bool $useFullyQualifiedUrls = false, bool $showTitle = true, bool $showArtworkStatus = true) + * @method static string ProjectForm(Project $project, $areFieldsRequired = true, $isEditForm = false) + * @method static string ProjectsTable(array $projects, bool $includeTitle = true, bool $includeStatus = true, bool $showEditButton = false) + * @method static string RealisticEbook(Ebook $ebook) + * @method static string RssEntry(Ebook $entry) + * @method static string RssFeed(string $title, string $description, DateTimeImmutable $updated, string $url, array $entries) + * @method static string SearchForm(string $query, array $tags, Enums\EbookSortType $sort, Enums\ViewType $view, int $perPage) + * @method static string UserForm(User $user, Enums\PasswordActionType $passwordAction, bool $generateNewUuid, bool $isEditForm = false) + * @method static string WantedEbooksList(array $ebooks, bool $showPlaceholderMetadata) + */ +class Template extends TemplateBase{ /** * Redirect the user to the login page. * diff --git a/lib/TemplateBase.php b/lib/TemplateBase.php new file mode 100644 index 00000000..a0d028a1 --- /dev/null +++ b/lib/TemplateBase.php @@ -0,0 +1,80 @@ + + * + * + * + * + * ```` + */ +abstract class TemplateBase{ + /** + * @param array $arguments + */ + public static function __callStatic(string $function, array $arguments): string{ + // Expand the passed variables to make them available to the included template. + // We use these funny names so that we can use `name` and `value` as template variables if we want to. + foreach($arguments as $innerName => $innerValue){ + $$innerName = $innerValue; + } + + ob_start(); + include(TEMPLATES_PATH . '/' . $function . '.php'); + $contents = ob_get_contents() ?: ''; + ob_end_clean(); + + return $contents; + } + + /** + * Exit the script while outputting the given HTTP code. + * + * @param bool $showPage If **`TRUE`**, show a special page given the HTTP code (like a 404 page). + * + * @return never + */ + public static function ExitWithCode(Enums\HttpCode $httpCode, bool $showPage = true, Enums\HttpRequestType $requestType = Enums\HttpRequestType::Web): void{ + http_response_code($httpCode->value); + + if($requestType == Enums\HttpRequestType::Web && $showPage){ + switch($httpCode){ + case Enums\HttpCode::Forbidden: + include(WEB_ROOT . '/403.php'); + break; + case Enums\HttpCode::NotFound: + include(WEB_ROOT . '/404.php'); + break; + } + } + + exit(); + } +} diff --git a/scripts/ingest-fa-payments b/scripts/ingest-fa-payments index 49f900a8..b2f227ec 100755 --- a/scripts/ingest-fa-payments +++ b/scripts/ingest-fa-payments @@ -217,8 +217,8 @@ catch(Exception $ex){ $em = new Email(true); $em->To = ADMIN_EMAIL_ADDRESS; $em->Subject = 'Ingesting FA donations failed'; - $em->Body = Template::EmailDonationProcessingFailed(['exception' => preg_replace('/^/m', "\t", $exceptionString)]); - $em->TextBody = Template::EmailDonationProcessingFailedText(['exception' => preg_replace('/^/m', "\t", $exceptionString)]); + $em->Body = Template::EmailDonationProcessingFailed(exception: preg_replace('/^/m', "\t", $exceptionString)); + $em->TextBody = Template::EmailDonationProcessingFailedText(exception: preg_replace('/^/m', "\t", $exceptionString)); $em->Send(); throw $ex; diff --git a/scripts/process-pending-payments b/scripts/process-pending-payments index 039f7dd4..522a49a3 100755 --- a/scripts/process-pending-payments +++ b/scripts/process-pending-payments @@ -277,8 +277,8 @@ try{ $em->To = ADMIN_EMAIL_ADDRESS; $em->From = ADMIN_EMAIL_ADDRESS; $em->Subject = 'New Patrons Circle member'; - $em->Body = Template::EmailAdminNewPatron(['patron' => $patron, 'payment' => $payment]); - $em->TextBody = Template::EmailAdminNewPatronText(['patron' => $patron, 'payment' => $payment]);; + $em->Body = Template::EmailAdminNewPatron(patron: $patron, payment: $payment); + $em->TextBody = Template::EmailAdminNewPatronText(patron: $patron, payment: $payment);; $em->Send(); } } @@ -324,8 +324,8 @@ catch(Exception $ex){ $em = new Email(true); $em->To = ADMIN_EMAIL_ADDRESS; $em->Subject = 'Donation processing failed'; - $em->Body = Template::EmailDonationProcessingFailed(['exception' => preg_replace('/^/m', "\t", $exceptionString)]); - $em->TextBody = Template::EmailDonationProcessingFailedText(['exception' => preg_replace('/^/m', "\t", $exceptionString)]); + $em->Body = Template::EmailDonationProcessingFailed(exception: preg_replace('/^/m', "\t", $exceptionString)); + $em->TextBody = Template::EmailDonationProcessingFailedText(exception: preg_replace('/^/m', "\t", $exceptionString)); $em->Send(); throw $ex; diff --git a/templates/ArtworkForm.php b/templates/ArtworkForm.php index e29abf72..e39fc800 100644 --- a/templates/ArtworkForm.php +++ b/templates/ArtworkForm.php @@ -1,14 +1,9 @@ Artist = new Artist(); -} - -$isEditForm = $isEditForm ?? false; +$isEditForm ??= false; ?>
Artist details diff --git a/templates/ArtworkList.php b/templates/ArtworkList.php index 846c5c9e..22eb6c69 100644 --- a/templates/ArtworkList.php +++ b/templates/ArtworkList.php @@ -4,33 +4,33 @@ */ ?>
    - - + EbookUrl !== null){ - $class .= ' in-use'; - } + if($artwork->EbookUrl !== null){ + $class .= ' in-use'; + } - switch($artwork->Status){ - case Enums\ArtworkStatusType::Unverified: - $class .= ' unverified'; - break; + switch($artwork->Status){ + case Enums\ArtworkStatusType::Unverified: + $class .= ' unverified'; + break; - case Enums\ArtworkStatusType::Declined: - $class .= ' declined'; - break; - } + case Enums\ArtworkStatusType::Declined: + $class .= ' declined'; + break; + } - $class = trim($class); - ?> - class=""> - - - - - - - - + $class = trim($class); + ?> + class=""> + + + + + + + +
diff --git a/templates/ArtworkStatus.php b/templates/ArtworkStatus.php deleted file mode 100644 index 4db717b7..00000000 --- a/templates/ArtworkStatus.php +++ /dev/null @@ -1,16 +0,0 @@ - -Status->value) ?> -EbookUrl !== null){ ?> - — in use by - Ebook !== null && $artwork->Ebook->Url !== null){ ?> - - Ebook->Title) ?> - Ebook->IsPlaceholder()){ ?>(unreleased) - - EbookUrl) ?> (unreleased) - - diff --git a/templates/AtomFeed.php b/templates/AtomFeed.php index b34f9c6f..c5b19160 100644 --- a/templates/AtomFeed.php +++ b/templates/AtomFeed.php @@ -8,7 +8,7 @@ * @var array $entries */ -$subtitle = $subtitle ?? null; +$subtitle ??= null; // Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. @@ -28,6 +28,6 @@ print("\n"); - $entry]) ?> + diff --git a/templates/CollectionDescriptor.php b/templates/CollectionDescriptor.php index 3c0bc537..2f93d4b5 100644 --- a/templates/CollectionDescriptor.php +++ b/templates/CollectionDescriptor.php @@ -1,12 +1,17 @@ Collection; $sequenceNumber = $collectionMembership?->SequenceNumber; ?> - in thePart of the Name)) ?> -Type !== null){ ?> + + in thePart of the Name)) ?> + +Type !== null){ ?> Name), mb_strtolower($collection->Type->value), -strlen(mb_strtolower($collection->Type->value))) !== 0){ ?>Type->value ?> collection diff --git a/templates/DonationCounter.php b/templates/DonationCounter.php index 704349fa..ade36219 100644 --- a/templates/DonationCounter.php +++ b/templates/DonationCounter.php @@ -4,8 +4,8 @@ if(!DONATION_DRIVE_COUNTER_ENABLED || ($autoHide ?? (HttpInput::Bool(COOKIE, 'hi return; } -$autoHide = $autoHide ?? true; -$showDonateButton = $showDonateButton ?? true; +$autoHide ??= true; +$showDonateButton ??= true; $current = 0; if(NOW < DONATION_DRIVE_COUNTER_START || NOW > DONATION_DRIVE_COUNTER_END){ diff --git a/templates/DonationProgress.php b/templates/DonationProgress.php index 04936cce..d347503b 100644 --- a/templates/DonationProgress.php +++ b/templates/DonationProgress.php @@ -13,8 +13,8 @@ if( return; } -$autoHide = $autoHide ?? true; -$showDonateButton = $showDonateButton ?? true; +$autoHide ??= true; +$showDonateButton ??= true; $deadline = $donationDrive->End->format('F j'); $timeLeft = NOW->diff($donationDrive->End); diff --git a/templates/EbookCarousel.php b/templates/EbookCarousel.php index 7165c029..5bba5450 100644 --- a/templates/EbookCarousel.php +++ b/templates/EbookCarousel.php @@ -3,7 +3,7 @@ * @var array $carousel */ -$isMultiSize = $isMultiSize ?? false; +$isMultiSize ??= false; ?> 0){ ?>
    diff --git a/templates/EbookGrid.php b/templates/EbookGrid.php index c8df24e5..1069876c 100644 --- a/templates/EbookGrid.php +++ b/templates/EbookGrid.php @@ -1,11 +1,11 @@ $ebooks + * @var ?Collection $collection */ -$view = $view ?? Enums\ViewType::Grid; -$collection = $collection ?? null; +$view ??= Enums\ViewType::Grid; +$collection ??= null; ?>
      typeof="schema:BookSeries" about="Url ?>"> diff --git a/templates/EbookMetadata.php b/templates/EbookMetadata.php index 2d70a673..1a36ecaa 100644 --- a/templates/EbookMetadata.php +++ b/templates/EbookMetadata.php @@ -3,7 +3,7 @@ * @var Ebook $ebook */ -$showPlaceholderMetadata = $showPlaceholderMetadata ?? false; +$showPlaceholderMetadata ??= false; ?>

      Metadata

      diff --git a/templates/EbookPlaceholderForm.php b/templates/EbookPlaceholderForm.php index 315e8cda..9ea69ca1 100644 --- a/templates/EbookPlaceholderForm.php +++ b/templates/EbookPlaceholderForm.php @@ -1,7 +1,10 @@
      Contributors @@ -204,7 +207,7 @@ $showProjectForm = $showProjectForm ?? true; />
      - $ebook->ProjectInProgress, 'areFieldsRequired' => false]) ?> + ProjectInProgress ?? new Project(), areFieldsRequired: false) ?>
      diff --git a/templates/EmailFooter.php b/templates/EmailFooter.php index d648d7dd..2d4f86b7 100644 --- a/templates/EmailFooter.php +++ b/templates/EmailFooter.php @@ -1,5 +1,5 @@
      diff --git a/templates/EmailHeader.php b/templates/EmailHeader.php index 26907699..97fc73e9 100644 --- a/templates/EmailHeader.php +++ b/templates/EmailHeader.php @@ -1,7 +1,11 @@ @@ -38,7 +42,7 @@ $hasAdminTable = $hasAdminTable ?? false; -webkit-text-size-adjust: none; } - + div.body.letterhead{ background-image: url("https://standardebooks.org/images/logo-email.png"); background-position: top 2em center; @@ -222,7 +226,7 @@ $hasAdminTable = $hasAdminTable ?? false; color: #4f9d85; } - + div.body.letterhead{ background-image: url("https://standardebooks.org/images/logo-email-dark.png"); } @@ -235,7 +239,7 @@ $hasAdminTable = $hasAdminTable ?? false; -
      +

      ‌ 

      diff --git a/templates/EmailManagerNewProject.php b/templates/EmailManagerNewProject.php index 517c64da..54662b8e 100644 --- a/templates/EmailManagerNewProject.php +++ b/templates/EmailManagerNewProject.php @@ -4,9 +4,9 @@ * @var string $role * @var User $user */ -?> true, 'letterhead' => true]) ?> +?>

      You’ve been assigned a new ebook project to :

      - $project, 'useFullyQualifiedUrls' => true, 'showArtworkStatus' => false]) ?> +

      If you’re unable to this ebook project, email the Editor-in-Chief and we’ll reassign it.

      • @@ -20,4 +20,4 @@

      - false]) ?> + diff --git a/templates/EmailPatronsCircleCompleted.php b/templates/EmailPatronsCircleCompleted.php index 9d0bb848..e1f30c8e 100644 --- a/templates/EmailPatronsCircleCompleted.php +++ b/templates/EmailPatronsCircleCompleted.php @@ -2,7 +2,7 @@ /** * @var int $ebooksThisYear */ -?> true]) ?> +?>

      Hello,

      Last year, your generous donation to Standard Ebooks made it possible for us to continue producing beautiful ebook editions for free distribution.

      It also allowed me to add you to our Patrons Circle, a group of donors who are honored on our masthead, and who have a direct voice in the future of our ebook catalog, for one year.

      diff --git a/templates/EmailPatronsCircleRecurringCompleted.php b/templates/EmailPatronsCircleRecurringCompleted.php index b8ecda44..ebff2166 100644 --- a/templates/EmailPatronsCircleRecurringCompleted.php +++ b/templates/EmailPatronsCircleRecurringCompleted.php @@ -1,4 +1,4 @@ - true]) ?> +

      Hello,

      I couldn’t help but notice that your monthly donation to Standard Ebooks has recently ended. Your generous donation allowed me to add you to our Patrons Circle, a group of donors who are honored on our masthead, and who have a direct voice in the future of our ebook catalog.

      Oftentimes credit cards will expire, or recurring charges will get accidentally canceled for any number of nebulous administrative reasons. If you didn’t mean to cancel your recurring donation—and thus your Patrons Circle membership—now’s a great time to renew it.

      diff --git a/templates/Header.php b/templates/Header.php index 87a89272..73d6d6bd 100644 --- a/templates/Header.php +++ b/templates/Header.php @@ -2,18 +2,32 @@ use Safe\DateTimeImmutable; use function Safe\filemtime; -$title = $title ?? ''; -$highlight = $highlight ?? ''; -$description = $description ?? ''; -$manual = $manual ?? false; +/** + * @var ?string $title + * @var ?string $highlight + * @var ?string $description + * @var ?string $feedUrl + * @var ?string $feedTitle + * @var ?string $downloadUrl + * @var ?string $canonicalUrl + * @var ?string $coverUrl + */ + +$title ??= null; +$highlight ??= null; +$description ??= null; +$feedUrl ??= null; +$feedTitle ??= null; +$downloadUrl ??= null; +$canonicalUrl ??= null; +$coverUrl ??= null; +$css ??= []; +$isManual ??= false; +$isXslt ??= false; +$isErrorPage ??= false; +$ogType ??= 'website'; + $colorScheme = Enums\ColorSchemeType::tryFrom(HttpInput::Str(COOKIE, 'color-scheme') ?? Enums\ColorSchemeType::Auto->value); -$isXslt = $isXslt ?? false; -$feedUrl = $feedUrl ?? null; -$feedTitle = $feedTitle ?? ''; -$isErrorPage = $isErrorPage ?? false; -$downloadUrl = $downloadUrl ?? null; -$canonicalUrl = $canonicalUrl ?? null; -$css = $css ?? []; $showPublicDomainDayBanner = PD_NOW > new DateTimeImmutable('January 1, 8:00 AM', SITE_TZ) && PD_NOW < new DateTimeImmutable('January 14', LATEST_CONTINENTAL_US_TZ) && !(HttpInput::Bool(COOKIE, 'hide-public-domain-day-banner') ?? false); // As of Sep. 2022, all versions of Safari have a bug where if the page is served as XHTML, then `` elements download all ``s instead of the first supported match. @@ -41,8 +55,8 @@ if(!$isXslt){ - <? if($title != ''){ ?><?= Formatter::EscapeHtml($title) ?> - <? } ?>Standard Ebooks: Free and liberated ebooks, carefully produced for the true book lover - + <? if($title !== null){ ?><?= Formatter::EscapeHtml($title) ?> - <? } ?>Standard Ebooks: Free and liberated ebooks, carefully produced for the true book lover + @@ -59,7 +73,7 @@ if(!$isXslt){ - + @@ -68,7 +82,7 @@ if(!$isXslt){ - + @@ -90,8 +104,8 @@ if(!$isXslt){ - - + + diff --git a/templates/OpdsAcquisitionFeed.php b/templates/OpdsAcquisitionFeed.php index 3e9812f6..aa6acd60 100644 --- a/templates/OpdsAcquisitionFeed.php +++ b/templates/OpdsAcquisitionFeed.php @@ -3,7 +3,7 @@ * Notes: * * - *All* OPDS feeds must contain a `rel="http://opds-spec.org/crawlable"` link pointing to the `/feeds/opds/all` feed. - * - The `` element is required to note this as a "Complete Acquisition Feeds"; see . + * - The `` element is required to note this as a "Complete Acquisition Feed"; see . */ /** @@ -16,8 +16,8 @@ * @var array $entries */ -$isCrawlable = $isCrawlable ?? false; -$subtitle = $subtitle ?? null; +$isCrawlable ??= false; +$subtitle ??= null; // Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. @@ -45,6 +45,6 @@ print("\n"); - $entry]) ?> + diff --git a/templates/OpdsNavigationFeed.php b/templates/OpdsNavigationFeed.php index d85a3363..46211991 100644 --- a/templates/OpdsNavigationFeed.php +++ b/templates/OpdsNavigationFeed.php @@ -9,7 +9,7 @@ * @var array $entries */ -$subtitle = $subtitle ?? null; +$subtitle ??= null; // Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. diff --git a/templates/ProjectDetailsTable.php b/templates/ProjectDetailsTable.php index c0fbfa35..6a45bc8c 100644 --- a/templates/ProjectDetailsTable.php +++ b/templates/ProjectDetailsTable.php @@ -1,13 +1,13 @@ diff --git a/templates/ProjectForm.php b/templates/ProjectForm.php index 45da1f50..933ab78a 100644 --- a/templates/ProjectForm.php +++ b/templates/ProjectForm.php @@ -1,10 +1,14 @@ diff --git a/templates/ProjectsTable.php b/templates/ProjectsTable.php index c9478b4f..b15715ce 100644 --- a/templates/ProjectsTable.php +++ b/templates/ProjectsTable.php @@ -3,9 +3,9 @@ * @var array $projects */ -$includeTitle = $includeTitle ?? true; -$includeStatus = $includeStatus ?? true; -$showEditButton = $showEditButton ?? false; +$includeTitle ??= true; +$includeStatus ??= true; +$showEditButton ??= false; ?>
      diff --git a/templates/RssFeed.php b/templates/RssFeed.php index 74fff983..733dc060 100644 --- a/templates/RssFeed.php +++ b/templates/RssFeed.php @@ -31,7 +31,7 @@ print("\n"); 144 - $entry]) ?> + diff --git a/templates/SearchForm.php b/templates/SearchForm.php index ae93200b..40458556 100644 --- a/templates/SearchForm.php +++ b/templates/SearchForm.php @@ -1,5 +1,6 @@ $tags * @var Enums\EbookSortType $sort * @var Enums\ViewType $view @@ -18,13 +19,13 @@ $isAllSelected = sizeof($tags) == 0 || in_array('all', $tags); - + diff --git a/www/artworks/index.php b/www/artworks/index.php index 9f59593c..d2c5980b 100644 --- a/www/artworks/index.php +++ b/www/artworks/index.php @@ -136,7 +136,7 @@ catch(Exceptions\PageOutOfBoundsException){ header('Location: ' . $url); exit(); } -?> $pageTitle, 'css' => ['/css/artwork.css'], 'description' => $pageDescription, 'canonicalUrl' => $canonicalUrl]) ?> +?>

      Browse U.S. Public Domain Artwork

      @@ -199,7 +199,7 @@ catch(Exceptions\PageOutOfBoundsException){

      No artwork matched your filters. You can try different filters, or browse all artwork.

      - $artworks]) ?> + 0){ ?> diff --git a/www/artworks/new.php b/www/artworks/new.php index 163acaec..5217b0bb 100644 --- a/www/artworks/new.php +++ b/www/artworks/new.php @@ -1,17 +1,17 @@ Benefits->CanUploadArtwork){ throw new Exceptions\InvalidPermissionsException(); } @@ -46,25 +46,22 @@ catch(Exceptions\InvalidPermissionsException){ ?> 'Submit an Artwork', - 'css' => ['/css/artwork.css'], - 'highlight' => '', - 'description' => 'Submit public domain artwork to the database for use as cover art.' - ] + title: 'Submit an Artwork', + css: ['/css/artwork.css'], + description: 'Submit public domain artwork to the database for use as cover art.' ) ?>

      Submit an Artwork

      - $exception]) ?> +

      Artwork submitted!

      - $artwork]) ?> +
      diff --git a/www/authors/get.php b/www/authors/get.php index 4e3d2320..8f57f8d1 100644 --- a/www/authors/get.php +++ b/www/authors/get.php @@ -31,7 +31,7 @@ try{ catch(Exceptions\AuthorNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> 'Ebooks by ' . $author, 'feedUrl' => str_replace('/ebooks/', '/authors/', $authorUrl), 'feedTitle' => 'Standard Ebooks - Ebooks by ' . $author, 'highlight' => 'ebooks', 'description' => 'All of the Standard Ebooks ebooks by ' . $author, 'canonicalUrl' => SITE_URL . $authorUrl]) ?> +?>

      Ebooks by AuthorsHtml ?>

      @@ -40,7 +40,7 @@ catch(Exceptions\AuthorNotFoundException){ Feeds for this author

      - $ebooks, 'view' => Enums\ViewType::Grid]) ?> +

      We also have bulk ebook downloads and a list of collections available, as well as ebook catalog feeds for use directly in your ereader app or RSS reader.

      diff --git a/www/blog/death-and-beauty-in-the-alps.php b/www/blog/death-and-beauty-in-the-alps.php index c945aece..a620cf48 100644 --- a/www/blog/death-and-beauty-in-the-alps.php +++ b/www/blog/death-and-beauty-in-the-alps.php @@ -2,7 +2,7 @@ $ebookIds = [1085, 1052]; $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSql($ebookIds), $ebookIds, Ebook::class); ?> - 'Death and Beauty in the Alps', 'css' => ['/css/blog.css'], 'highlight' => '', 'description' => '']) ?> +
      @@ -31,7 +31,7 @@ $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSq

      Scrambles Amongst the Alps has something of both “darkling thrush” and “darkling plain”: an “eternal note of sadness” following terrible loss, but also real, if fleeting, notes of joy in laboring for, and realizing, a hope widely believed impossible.

      The woe of its best-known story is confounded by lesser-known, brighter elements—even if they’re only “thin atomies” in comparison—praising the worth of the endeavour and the value of the toil it required, despite the cruel hand the explorers were dealt.

      Free ebooks in this post

      - $carousel]) ?> +
      diff --git a/www/blog/edith-whartons-vision-of-literary-art.php b/www/blog/edith-whartons-vision-of-literary-art.php index f8991d34..4f845a31 100644 --- a/www/blog/edith-whartons-vision-of-literary-art.php +++ b/www/blog/edith-whartons-vision-of-literary-art.php @@ -2,7 +2,7 @@ $ebookIds = [288, 485, 289, 908, 565, 2114]; $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSql($ebookIds), $ebookIds, Ebook::class); ?> - 'Edith Wharton’s Vision of Literary Art', 'css' => ['/css/blog.css'], 'highlight' => '', 'description' => '']) ?> +
      @@ -33,7 +33,7 @@ $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSq

      Hudson River Bracketed is long out of print. Wharton patently lost the critical and commercial “Wettgesang” of the 1930s; even her sympathizers tend to admit that she’s in no way a star of that period, so the analogy to the Prologue in Heaven falls (or sounds) very flat.

      But whether Wharton’s second-last work falls entirely flat too is something that can be judged, if at all, only in the old way, by reading it. This wasn’t very easy to do until January 1, but now you can read our new ebook edition for free at Standard Ebooks.

      Free ebooks in this post

      - $carousel]) ?> +
      diff --git a/www/blog/index.php b/www/blog/index.php index 03a3e173..addd8325 100644 --- a/www/blog/index.php +++ b/www/blog/index.php @@ -1,4 +1,4 @@ - 'Blog', 'highlight' => '', 'description' => 'The Standard Ebooks blog.']) ?> +

      Blog

      diff --git a/www/blog/joyces-ulysses-the-rubaiyat-and-yes.php b/www/blog/joyces-ulysses-the-rubaiyat-and-yes.php index dd4b98bd..4867c01e 100644 --- a/www/blog/joyces-ulysses-the-rubaiyat-and-yes.php +++ b/www/blog/joyces-ulysses-the-rubaiyat-and-yes.php @@ -2,7 +2,7 @@ $ebookIds = [565, 778, 561, 1059]; $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSql($ebookIds), $ebookIds, Ebook::class); ?> - 'Joyce’s Ulysses, the Rubáiyát, and “Yes”', 'css' => ['/css/blog.css'], 'highlight' => '', 'description' => '']) ?> +
      @@ -44,7 +44,7 @@ $carousel = Db::Query('SELECT * from Ebooks where EbookId in ' . Db::CreateSetSq

      Speaking of consonance: Brown says, citing Ellmann, that the last record Joyce heard before he died was a performance of Lehmann’s setting of Fitzgerald’s Omar. As is often the case with Ellmann, this might not be true, but it’s not absurd to suppose that it could be.

      And as very often with Ulysses, what first seems like nothing, or like material for a joke, may also turn out to be something else too, even something that matters. If Ulysses doesn’t entirely affirm life, then it does, in this respect at least, reflect it.

      Free ebooks in this post

      - $carousel]) ?> +
      diff --git a/www/blog/public-domain-day-2025.php b/www/blog/public-domain-day-2025.php index e0ba6f1a..aec3dd75 100644 --- a/www/blog/public-domain-day-2025.php +++ b/www/blog/public-domain-day-2025.php @@ -100,7 +100,7 @@ foreach($ebooks as $ebook){ ksort($ebooksWithDescriptions); -?> 'Public Domain Day 2025 in Literature - Blog', 'highlight' => '', 'description' => 'Read about the new ebooks Standard Ebooks is releasing for Public Domain Day 2025!', 'css' => ['/css/public-domain-day.css']]) ?> +?>
      @@ -133,7 +133,7 @@ ksort($ebooksWithDescriptions);
    1. diff --git a/www/bulk-downloads/collection.php b/www/bulk-downloads/collection.php index 94be94e0..7a4990d3 100644 --- a/www/bulk-downloads/collection.php +++ b/www/bulk-downloads/collection.php @@ -27,7 +27,7 @@ catch(Safe\Exceptions\ApcuException){ $title = preg_replace('/s$/', '', ucfirst($class)); -?> 'Downloads by ' . $title, 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks in a given collection.']) ?> +?>

      Down­loads by

      @@ -80,7 +80,10 @@ $title = preg_replace('/s$/', '', ucfirst($class));
    2. Status: $artwork]) ?> + Status->value) ?> + EbookUrl !== null){ ?> + — in use by + Ebook !== null && $artwork->Ebook->Url !== null){ ?> + + Ebook->Title) ?> + Ebook->IsPlaceholder()){ ?>(unreleased) + + EbookUrl) ?> (unreleased) + + +
      - $title, 'collections' => $collection]); ?> + $collection */ + ?> +
      diff --git a/www/bulk-downloads/download.php b/www/bulk-downloads/download.php index 60f62adc..0d3c7c8d 100644 --- a/www/bulk-downloads/download.php +++ b/www/bulk-downloads/download.php @@ -54,7 +54,7 @@ catch(Exceptions\InvalidFileException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> 'Downloading Ebook Collections', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?> +?>

      Downloading Ebook Collections

      diff --git a/www/bulk-downloads/get.php b/www/bulk-downloads/get.php index 2ffdb110..0e657279 100644 --- a/www/bulk-downloads/get.php +++ b/www/bulk-downloads/get.php @@ -1,13 +1,12 @@ Benefits->CanBulkDownload){ $canDownload = true; } @@ -33,10 +32,6 @@ try{ break; } } - - if($collection === null){ - throw new Exceptions\CollectionNotFoundException(); - } } if($authorUrlName !== null){ @@ -65,6 +60,10 @@ try{ throw new Exceptions\AuthorNotFoundException(); } } + + if($collection === null){ + throw new Exceptions\CollectionNotFoundException(); + } } catch(Exceptions\AuthorNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); @@ -73,17 +72,17 @@ catch(Exceptions\CollectionNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> '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.

      Patrons circle members can download zip files containing all of the ebooks in a collection. You can join the Patrons Circle with a small donation in support of our continuing mission to create free, beautiful digital literature.

      - 'Collection', 'collections' => [$collection]]); ?> +
      diff --git a/www/bulk-downloads/index.php b/www/bulk-downloads/index.php index 0b446d9f..c9473204 100644 --- a/www/bulk-downloads/index.php +++ b/www/bulk-downloads/index.php @@ -4,7 +4,7 @@ if(Session::$User?->Benefits->CanBulkDownload){ $canDownload = true; } -?> 'Bulk Ebook Downloads', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?> +?>

      Bulk Ebook Down­loads

      diff --git a/www/collections/get.php b/www/collections/get.php index 12780bed..13222702 100644 --- a/www/collections/get.php +++ b/www/collections/get.php @@ -16,7 +16,7 @@ try{ catch(Exceptions\CollectionNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> $pageTitle, 'feedUrl' => $feedUrl, 'feedTitle' => $feedTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?> +?>

      @@ -32,7 +32,7 @@ catch(Exceptions\CollectionNotFoundException){ Ebooks) == 0){ ?>

      No ebooks matched your filters. You can try different filters, or browse all of our ebooks.

      - $collection->Ebooks, 'view' => Enums\ViewType::Grid, 'collection' => $collection]) ?> + Ebooks, view: Enums\ViewType::Grid, collection: $collection) ?> Benefits->CanEditCollections){ ?> diff --git a/www/collections/index.php b/www/collections/index.php index d3479ae1..b8d7329c 100644 --- a/www/collections/index.php +++ b/www/collections/index.php @@ -1,7 +1,7 @@ 'Ebook Collections', 'highlight' => '', 'description' => 'Browse collections of Standard Ebooks.']) ?> +?>

      Ebook Collections

      diff --git a/www/contribute/a-basic-standard-ebooks-source-folder.php b/www/contribute/a-basic-standard-ebooks-source-folder.php index 76efa9e0..3795494d 100644 --- a/www/contribute/a-basic-standard-ebooks-source-folder.php +++ b/www/contribute/a-basic-standard-ebooks-source-folder.php @@ -1,4 +1,4 @@ - 'A Basic Standard Ebooks Source Folder', 'manual' => true, 'highlight' => 'contribute', 'description' => 'All Standard Ebooks source folders have the same basic structure, described here.']) ?> +

      A Basic Standard Ebooks Source Folder

      diff --git a/www/contribute/collections-policy.php b/www/contribute/collections-policy.php index 79a57c62..62c8fbdd 100644 --- a/www/contribute/collections-policy.php +++ b/www/contribute/collections-policy.php @@ -1,4 +1,4 @@ - 'Collections Policy', 'highlight' => 'contribute', 'description' => 'Standard Ebooks only accepts certain kinds of ebooks for production and hosting. This is the full list.']) ?> +
      diff --git a/www/contribute/how-tos/common-issues-when-working-on-public-domain-ebooks.php b/www/contribute/how-tos/common-issues-when-working-on-public-domain-ebooks.php index 013de5d5..2b906059 100644 --- a/www/contribute/how-tos/common-issues-when-working-on-public-domain-ebooks.php +++ b/www/contribute/how-tos/common-issues-when-working-on-public-domain-ebooks.php @@ -1,4 +1,4 @@ - 'Common Issues When Working on Public Domain Ebooks', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A list of common issues encountered when converting from public domain transcriptions.']) ?> +

      Common Issues When Working on Public Domain Ebooks

      diff --git a/www/contribute/how-tos/how-to-choose-and-create-a-cover-image.php b/www/contribute/how-tos/how-to-choose-and-create-a-cover-image.php index 16a59635..056f4c38 100644 --- a/www/contribute/how-tos/how-to-choose-and-create-a-cover-image.php +++ b/www/contribute/how-tos/how-to-choose-and-create-a-cover-image.php @@ -1,4 +1,4 @@ - 'How to Choose and Create a Cover Image', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to choosing, clearing, and formatting your cover image.']) ?> +

      How to Choose and Create a Cover Image

      diff --git a/www/contribute/how-tos/how-to-conquer-complex-drama-formatting.php b/www/contribute/how-tos/how-to-conquer-complex-drama-formatting.php index a4993a66..7b383125 100644 --- a/www/contribute/how-tos/how-to-conquer-complex-drama-formatting.php +++ b/www/contribute/how-tos/how-to-conquer-complex-drama-formatting.php @@ -1,4 +1,4 @@ - 'How to Conquer Complex Drama Formatting', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to formatting any complex plays or dramatic dialog sections.']) ?> +

      How to Conquer Complex Drama Formatting

      diff --git a/www/contribute/how-tos/how-to-create-figures-for-music-scores.php b/www/contribute/how-tos/how-to-create-figures-for-music-scores.php index 182c9fd3..08f79d72 100644 --- a/www/contribute/how-tos/how-to-create-figures-for-music-scores.php +++ b/www/contribute/how-tos/how-to-create-figures-for-music-scores.php @@ -1,4 +1,4 @@ - 'How to Create Figures for Music Scores', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to producing SVG figures of music notation.']) ?> +

      How to Create Figures for Music Scores

      diff --git a/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php b/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php index 248421f8..8b37ec4b 100644 --- a/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php +++ b/www/contribute/how-tos/how-to-create-svgs-from-maps-with-several-colors.php @@ -1,4 +1,4 @@ - '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.']) ?> +

      How to Create SVGs from Maps with Several Colors

      diff --git a/www/contribute/how-tos/how-to-review-an-ebook-production-for-publication.php b/www/contribute/how-tos/how-to-review-an-ebook-production-for-publication.php index 6823d4aa..064a07ca 100644 --- a/www/contribute/how-tos/how-to-review-an-ebook-production-for-publication.php +++ b/www/contribute/how-tos/how-to-review-an-ebook-production-for-publication.php @@ -1,4 +1,4 @@ - 'How to Review an Ebook Production for Publication', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to proofread and review an ebook production for publication.']) ?> +

      How to Review an Ebook Production for Publication

      diff --git a/www/contribute/how-tos/how-to-structure-and-style-large-poetic-productions.php b/www/contribute/how-tos/how-to-structure-and-style-large-poetic-productions.php index daa58741..49f493a8 100644 --- a/www/contribute/how-tos/how-to-structure-and-style-large-poetic-productions.php +++ b/www/contribute/how-tos/how-to-structure-and-style-large-poetic-productions.php @@ -1,4 +1,4 @@ - 'How to Structure and Style Large Poetic Productions', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A guide to formatting poetry collections, long narrative poems, and unusual poetic features.']) ?> +

      How to Structure and Style Large Poetic Productions

      diff --git a/www/contribute/how-tos/index.php b/www/contribute/how-tos/index.php index eb45b0a5..db62a5a3 100644 --- a/www/contribute/how-tos/index.php +++ b/www/contribute/how-tos/index.php @@ -1,4 +1,4 @@ - 'How-to Guides For Difficult Productions', 'manual' => true, 'highlight' => 'contribute', 'description' => 'Guides on how to produce more difficult productions.']) ?> +

      How-to Guides

      diff --git a/www/contribute/how-tos/things-to-look-out-for-when-proofreading.php b/www/contribute/how-tos/things-to-look-out-for-when-proofreading.php index cefbc511..0284ac98 100644 --- a/www/contribute/how-tos/things-to-look-out-for-when-proofreading.php +++ b/www/contribute/how-tos/things-to-look-out-for-when-proofreading.php @@ -1,4 +1,4 @@ - 'Things to Look Out For When Proofreading', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A list of things to look out for when proofreading.']) ?> +

      Things to Look Out For When Proofreading

      diff --git a/www/contribute/index.php b/www/contribute/index.php index 0049b533..6db7faa5 100644 --- a/www/contribute/index.php +++ b/www/contribute/index.php @@ -1,4 +1,4 @@ - 'Get Involved', 'highlight' => 'contribute', 'description' => 'Details on how to contribute your time and talent to the volunteer-driven Standard Ebooks project.']) ?> +
      diff --git a/www/contribute/producers.php b/www/contribute/producers.php index 5ea545bc..e245f7f7 100644 --- a/www/contribute/producers.php +++ b/www/contribute/producers.php @@ -1,4 +1,4 @@ - 'Producing an Ebook for Standard Ebooks', 'highlight' => 'contribute', 'description' => 'A high-level outline of the process of producing an ebook for Standard Ebooks.']) ?> +

      Producing an Ebook for Standard Ebooks

      diff --git a/www/contribute/producing-an-ebook-step-by-step.php b/www/contribute/producing-an-ebook-step-by-step.php index ebfbb77c..520a6b71 100644 --- a/www/contribute/producing-an-ebook-step-by-step.php +++ b/www/contribute/producing-an-ebook-step-by-step.php @@ -1,4 +1,4 @@ - 'Producing an Ebook, Step by Step', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A detailed step-by-step description of the complete process of producing an ebook for Standard Ebooks, start to finish.']) ?> +

      Producing an Ebook, Step by Step

      diff --git a/www/contribute/report-errors-upstream.php b/www/contribute/report-errors-upstream.php index bf91f7f7..58cfdcbe 100644 --- a/www/contribute/report-errors-upstream.php +++ b/www/contribute/report-errors-upstream.php @@ -1,4 +1,4 @@ - 'Report Errors Upstream', 'highlight' => 'contribute', 'description' => 'Our guide to reporting errors to Gutenberg and other sources.']) ?> +

      Report Errors Upstream

      diff --git a/www/contribute/report-errors.php b/www/contribute/report-errors.php index e3d123ee..67f59354 100644 --- a/www/contribute/report-errors.php +++ b/www/contribute/report-errors.php @@ -1,4 +1,4 @@ - 'Report Errors', 'highlight' => 'contribute', 'description' => 'How to report a typo or error you’ve found in a Standard Ebooks ebook.']) ?> +

      Report Errors

      diff --git a/www/contribute/spreadsheets.php b/www/contribute/spreadsheets.php index 1f72ec60..b4127a95 100644 --- a/www/contribute/spreadsheets.php +++ b/www/contribute/spreadsheets.php @@ -1,4 +1,4 @@ - 'Research Spreadsheets', 'highlight' => 'contribute', 'description' => 'A list of spreadsheets created and used by Standard Ebooks producers.']) ?> +

      Research Spreadsheets

      diff --git a/www/contribute/tips-for-editors-and-proofreaders.php b/www/contribute/tips-for-editors-and-proofreaders.php index 90e25ff3..282cf487 100644 --- a/www/contribute/tips-for-editors-and-proofreaders.php +++ b/www/contribute/tips-for-editors-and-proofreaders.php @@ -1,4 +1,4 @@ - 'Tips for Editors and Proofreaders', 'manual' => true, 'highlight' => 'contribute', 'description' => 'A list of tips and tricks for people who’d like to proofread a Standard Ebooks ebook.']) ?> +

      Tips for Editors and Proofreaders

      diff --git a/www/contribute/uncategorized-art-resources.php b/www/contribute/uncategorized-art-resources.php index a5f2c3d3..7fbe4458 100644 --- a/www/contribute/uncategorized-art-resources.php +++ b/www/contribute/uncategorized-art-resources.php @@ -1,4 +1,4 @@ - 'Uncategorized Art Resources', 'highlight' => 'contribute', 'description' => 'A list of US-PD art books for use when conducting cover art research.']) ?> +

      Uncategorized Art Resources

      diff --git a/www/contribute/wanted-ebooks.php b/www/contribute/wanted-ebooks.php index 134c7c19..06acfb56 100644 --- a/www/contribute/wanted-ebooks.php +++ b/www/contribute/wanted-ebooks.php @@ -3,7 +3,7 @@ $beginnerEbooks = Ebook::GetByIsWantedAndDifficulty(Enums\EbookPlaceholderDiffic $intermediateEbooks = Ebook::GetByIsWantedAndDifficulty(Enums\EbookPlaceholderDifficulty::Intermediate); $advancedEbooks = Ebook::GetByIsWantedAndDifficulty(Enums\EbookPlaceholderDifficulty::Advanced); ?> - 'Wanted Ebooks', 'highlight' => 'contribute', 'description' => 'A list of ebooks the Standard Ebooks editor would like to see produced, including suggestions for first-time producers.']) ?> +

      Wanted Ebooks

      @@ -18,13 +18,13 @@ $advancedEbooks = Ebook::GetByIsWantedAndDifficulty(Enums\EbookPlaceholderDiffic

      For your first production

      If nothing on the list below interests you, you can pitch us something else you’d like to work on.

      First productions should be on the shorter side (less than 100,000 words maximum) and without too many complex formatting issues like illustrations, significant endnotes, letters, poems, etc. Most short plain fiction novels fall in this category.

      - $beginnerEbooks, 'showPlaceholderMetadata' => Session::$User?->Benefits->CanEditEbookPlaceholders]) ?> + Benefits->CanEditEbookPlaceholders ?? false) ?>

      Moderate-difficulty productions

      - $intermediateEbooks, 'showPlaceholderMetadata' => Session::$User?->Benefits->CanEditEbookPlaceholders]) ?> + Benefits->CanEditEbookPlaceholders ?? false) ?>

      Advanced productions

      - $advancedEbooks, 'showPlaceholderMetadata' => Session::$User?->Benefits->CanEditEbookPlaceholders]) ?> + Benefits->CanEditEbookPlaceholders ?? false) ?>

      Jules Verne

      Verne has a complex publication and translation history. Please review these notes before starting any Verne books.

      diff --git a/www/donate/index.php b/www/donate/index.php index 62b2a4a3..720d95a3 100644 --- a/www/donate/index.php +++ b/www/donate/index.php @@ -5,15 +5,15 @@ $newsletterSubscriberCount = floor(Db::QueryInt(' where IsConfirmed = true ') / 100) * 100; -?> 'Donate', 'highlight' => 'donate', 'description' => 'Donate to Standard Ebooks.']) ?> +?>
      diff --git a/www/ebook-placeholders/new.php b/www/ebook-placeholders/new.php index 406c6d69..47674669 100644 --- a/www/ebook-placeholders/new.php +++ b/www/ebook-placeholders/new.php @@ -1,15 +1,6 @@ value); @@ -70,18 +71,15 @@ catch(Exceptions\InvalidPermissionsException){ } ?> 'Create an Ebook Placeholder', - 'css' => ['/css/ebook-placeholder.css', '/css/project.css'], - 'highlight' => '', - 'description' => 'Create a placeholder for an ebook not yet in the collection.' - ] + title: 'Create an Ebook Placeholder', + css: ['/css/ebook-placeholder.css', '/css/project.css'], + description: 'Create a placeholder for an ebook not yet in the collection.' ) ?>

      Create an Ebook Placeholder

      - $exception]) ?> + @@ -94,7 +92,7 @@ catch(Exceptions\InvalidPermissionsException){ - $ebook]) ?> + diff --git a/www/ebooks/download.php b/www/ebooks/download.php index e9819b55..8a2c59f5 100644 --- a/www/ebooks/download.php +++ b/www/ebooks/download.php @@ -61,7 +61,7 @@ try{ catch(Exceptions\InvalidFileException | Exceptions\EbookNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> 'Your Download Has Started!', 'downloadUrl' => $downloadUrl]) ?> +?>

      Your Download Has Started!

      diff --git a/www/ebooks/get.php b/www/ebooks/get.php index 52a828a3..8a9df252 100644 --- a/www/ebooks/get.php +++ b/www/ebooks/get.php @@ -71,7 +71,7 @@ catch(Exceptions\EbookNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?> strip_tags($ebook->TitleWithCreditsHtml) . ' - Free ebook download', 'ogType' => 'book', 'coverUrl' => $ebook->DistCoverUrl, 'highlight' => 'ebooks', 'description' => 'Free epub ebook download of the Standard Ebooks edition of ' . $ebook->Title . ': ' . $ebook->Description, 'canonicalUrl' => SITE_URL . $ebook->Url]) ?> +?>TitleWithCreditsHtml) . ' - Free ebook download', ogType: 'book', coverUrl: $ebook->DistCoverUrl, highlight: 'ebooks', description: 'Free epub ebook download of the Standard Ebooks edition of ' . $ebook->Title . ': ' . $ebook->Description, canonicalUrl: SITE_URL . $ebook->Url) ?>
      @@ -126,7 +126,7 @@ catch(Exceptions\EbookNotFoundException){ CollectionMemberships) > 0){ ?> CollectionMemberships as $collectionMembership){ ?>

      - $collectionMembership]) ?>. + .

      @@ -188,7 +188,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.

      - $ebook]) ?> +

      Download for ereaders

      @@ -383,13 +383,13 @@ catch(Exceptions\EbookNotFoundException){
      Benefits->CanEditEbooks){ ?> - $ebook]) ?> + 0){ ?>
      diff --git a/www/ebooks/index.php b/www/ebooks/index.php index 349b2284..966f64fb 100644 --- a/www/ebooks/index.php +++ b/www/ebooks/index.php @@ -7,8 +7,8 @@ $pages = 0; $perPage = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; $query = HttpInput::Str(GET, 'query') ?? ''; $tags = HttpInput::Array(GET, 'tags') ?? []; -$view = Enums\ViewType::tryFrom(HttpInput::Str(GET, 'view') ?? ''); -$sort = Enums\EbookSortType::tryFrom(HttpInput::Str(GET, 'sort') ?? ''); +$view = Enums\ViewType::tryFrom(HttpInput::Str(GET, 'view') ?? '') ?? Enums\ViewType::Grid; +$sort = Enums\EbookSortType::tryFrom(HttpInput::Str(GET, 'sort') ?? '') ?? Enums\EbookSortType::Default; $queryString = ''; $queryStringParams = []; $queryStringWithoutPage = ''; @@ -38,13 +38,8 @@ try{ $sort = Enums\EbookSortType::Newest; } - // If we're passed string values that are the same as the defaults, set them to null so that we can have cleaner query strings in the navigation footer. - if($view === Enums\ViewType::Grid){ - $view = null; - } - if(($sort == Enums\EbookSortType::Newest && $query == '') || ($sort == Enums\EbookSortType::Relevance && $query != '')){ - $sort = null; + $sort = Enums\EbookSortType::Default; } if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){ @@ -61,11 +56,12 @@ try{ $queryStringParams['tags'] = $tags; } - if($view !== null){ + // If we're passed string values that are the same as the defaults, don't include them in the query string so that we can have cleaner query strings in the navigation footer. + if($view != Enums\ViewType::Grid){ $queryStringParams['view'] = $view->value; } - if($sort !== null){ + if($sort != Enums\EbookSortType::Default){ $queryStringParams['sort'] = $sort->value; } @@ -134,7 +130,7 @@ catch(Exceptions\PageOutOfBoundsException){ header('Location: ' . $url); exit(); } -?> $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription, 'canonicalUrl' => $canonicalUrl]) ?> +?>

      @@ -142,11 +138,12 @@ catch(Exceptions\PageOutOfBoundsException){ - $query, 'tags' => $tags, 'sort' => $sort, 'view' => $view, 'perPage' => $perPage]) ?> + +

      No ebooks matched your filters. You can try different filters, or browse all of our ebooks.

      - $ebooks, 'view' => $view]) ?> + 0){ ?>