Add thank-you page to ebook downloads

This commit is contained in:
Alex Cabal 2024-02-25 20:12:20 -06:00
parent 29fc6f9ff2
commit 9437beaee9
9 changed files with 222 additions and 16 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
ebooks/*
www/ebooks/*
www/ebooks/*/
www/images/covers/*
www/feeds/opds/*.xml
www/feeds/opds/*/*.xml

View file

@ -218,6 +218,9 @@ Define webroot /standardebooks.org/web
# Redirect latest version of the manual
RewriteRule ^/manual/latest(.*) /manual/index.php?url=$1 [L]
# Rewrite ebook downloads
RewriteRule ^/ebooks/(.+?)/download$ /ebooks/download.php?url-path=$1 [QSA]
# List of specific URL rewrites
RewriteRule ^/contribute/accepted-ebooks/? /contribute/collections-policy [R=301,L]
RewriteRule ^/ebooks/aristotle/the-nicomachean-ethics(/?$|/.+?$) /ebooks/aristotle/nicomachean-ethics$1 [R=301,L]
@ -307,18 +310,25 @@ Define webroot /standardebooks.org/web
RewriteRule ^/artworks/([^/\.]+)/([^/\.]+)/edit$ /artworks/edit.php?artist-url-name=$1&artwork-url-name=$2 [L]
# Specific config for /bulk-downloads
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Specific config for /ebooks/<author>/<ebook>/downloads
<DirectoryMatch "^${webroot}/www/ebooks/.+">
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/bulk-downloads
XSendFilePath ${webroot}/www/ebooks
</DirectoryMatch>
# Specific config for /bulk-downloads
<DirectoryMatch "^${webroot}/www/bulk-downloads/">
# Both directives are required
XSendFile on
XSendFilePath ${webroot}/www/bulk-downloads
</DirectoryMatch>
# Specific config for /feeds
<DirectoryMatch "^${webroot}/www/feeds">
<DirectoryMatch "^${webroot}/www/feeds/">
# This must be defined at the top level /feeds/ directory
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/feeds
XSendFilePath ${webroot}/www/feeds
</DirectoryMatch>
</VirtualHost>

View file

@ -200,6 +200,9 @@ Define webroot /standardebooks.org/web
# Redirect latest version of the manual
RewriteRule ^/manual/latest(.*) /manual/index.php?url=$1 [L]
# Rewrite ebook downloads
RewriteRule ^/ebooks/(.+?)/download$ /ebooks/download.php?url-path=$1 [QSA]
# List of specific URL rewrites
RewriteRule ^/contribute/accepted-ebooks/? /contribute/collections-policy [R=301,L]
RewriteRule ^/ebooks/aristotle/the-nicomachean-ethics(/?$|/.+?$) /ebooks/aristotle/nicomachean-ethics$1 [R=301,L]
@ -289,18 +292,25 @@ Define webroot /standardebooks.org/web
RewriteRule ^/artworks/([^/\.]+)/([^/\.]+)/edit$ /artworks/edit.php?artist-url-name=$1&artwork-url-name=$2 [L]
# Specific config for /bulk-downloads
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Specific config for /ebooks/<author>/<ebook>/downloads
<DirectoryMatch "^${webroot}/www/ebooks/.+">
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/bulk-downloads
XSendFilePath ${webroot}/www/ebooks
</DirectoryMatch>
# Specific config for /bulk-downloads
<DirectoryMatch "^${webroot}/www/bulk-downloads/">
# Both directives are required
XSendFile on
XSendFilePath ${webroot}/www/bulk-downloads
</DirectoryMatch>
# Specific config for /feeds
<DirectoryMatch "^${webroot}/www/feeds">
<DirectoryMatch "^${webroot}/www/feeds/">
# This must be defined at the top level /feeds/ directory
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/feeds
XSendFilePath ${webroot}/www/feeds
</DirectoryMatch>
</VirtualHost>

15
lib/EbookFormat.php Normal file
View file

@ -0,0 +1,15 @@
<?
enum EbookFormat: string{
case Epub = 'epub';
case Azw3 = 'azw3';
case Kepub = 'kepub';
case AdvancedEpub = 'advanced-epub';
public function GetMimeType(): string{
return match($this){
self::Azw3 => 'application/x-mobi8-ebook',
default => 'application/epub+zip'
};
}
}

View file

@ -10,6 +10,7 @@ $isXslt = $isXslt ?? false;
$feedUrl = $feedUrl ?? null;
$feedTitle = $feedTitle ?? '';
$isErrorPage = $isErrorPage ?? false;
$downloadUrl = $downloadUrl ?? null;
// As of Sep 2022, all versions of Safari have a bug where if the page is served as XHTML,
// then <picture> elements download all <source>s instead of the first supported match.
@ -81,6 +82,9 @@ if(!$isXslt){
<meta content="@standardebooks" name="twitter:site"/>
<meta content="@standardebooks" name="twitter:creator"/>
<? } ?>
<? if($downloadUrl !== null){ ?>
<meta http-equiv="refresh" content="0; url=<?= Formatter::EscapeHtml($downloadUrl) ?>" />
<? } ?>
</head>
<body>
<header>

View file

@ -1490,7 +1490,8 @@ ol.ebooks-list > li p{
hyphens: none;
}
ol.ebooks-list > li img{
ol.ebooks-list > li img,
img.ebook{
box-sizing: border-box;
max-width: 100%;
height: auto;
@ -2905,6 +2906,21 @@ ul.feed p{
padding-bottom: 0;
}
.stinger{
font-family: "Crimson Pro", serif;
font-style: italic;
font-size: 2rem;
}
.thank-you-container{
display: flex;
gap: 2rem;
}
.thank-you-container picture{
flex-shrink: 0;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */
select[multiple]{
@ -3157,6 +3173,12 @@ ul.feed p{
}
}
@media(max-width: 860px){
.thank-you-container picture{
display: none;
}
}
@media(max-width: 730px){
form[action="/ebooks"]{
grid-template-columns: auto auto 1fr 1fr;

127
www/ebooks/download.php Normal file
View file

@ -0,0 +1,127 @@
<?
use function Safe\apcu_fetch;
// If the user is not logged in, or has less than some amount of downloads, show a thank-you page
$ebook = null;
$downloadCount = $_COOKIE['download-count'] ?? 0;
$showThankYouPage = $GLOBALS['User'] === null && $downloadCount < 5;
$downloadUrl = null;
try{
$urlPath = HttpInput::Str(GET, 'url-path') ?? null;
$format = EbookFormat::tryFrom(HttpInput::Str(GET, 'format') ?? '') ?? EbookFormat::Epub;
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath;
// Do we have the ebook cached?
try{
$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;
break;
case EbookFormat::Azw3:
$downloadUrl = $ebook->Azw3Url;
break;
case EbookFormat::AdvancedEpub:
$downloadUrl = $ebook->AdvancedEpubUrl;
break;
case EbookFormat::Epub:
default:
$downloadUrl = $ebook->EpubUrl;
break;
}
if(!$showThankYouPage){
// Download the file directly, without showing the thank you page
$downloadPath = WEB_ROOT . $downloadUrl;
if(!is_file($downloadPath)){
throw new Exceptions\InvalidFileException();
}
// Everything OK, serve the file using Apache.
// The xsendfile Apache module tells Apache to serve the file, including not-modified or etag headers.
// Much more efficient than reading it in PHP and outputting it that way.
header('X-Sendfile: ' . $downloadPath);
header('Content-Type: ' . $format->GetMimeType());
header('Content-Disposition: attachment; filename="' . basename($downloadPath) . '"');
exit();
}
// Increment local download count, expires in 2 weeks
$downloadCount++;
setcookie('download-count', (string)$downloadCount, ['expires' => strtotime('+2 week'), 'path' => '/', 'domain' => SITE_DOMAIN, 'secure' => true, 'httponly' => false, 'samesite' => 'Lax']);
if(!$showThankYouPage){
exit();
}
}
catch(Exceptions\InvalidFileException){
Template::Emit404();
}
?><?= Template::Header(['downloadUrl' => $downloadUrl]) ?>
<main class="donate">
<h1>Your download has started!</h1>
<div class="thank-you-container">
<picture>
<? if($ebook->CoverImage2xAvifUrl !== null){ ?><source srcset="<?= $ebook->CoverImage2xAvifUrl ?> 2x, <?= $ebook->CoverImageAvifUrl ?> 1x" type="image/avif"/><? } ?>
<source srcset="<?= $ebook->CoverImage2xUrl ?> 2x, <?= $ebook->CoverImageUrl ?> 1x" type="image/jpg"/>
<img class="ebook" src="<?= $ebook->CoverImage2xUrl ?>" alt="The cover for the Standard Ebooks edition of <?= Formatter::EscapeHtml(strip_tags($ebook->TitleWithCreditsHtml)) ?>" property="schema:image" height="335" width="224"/>
</picture>
<div>
<p class="stinger">Before you go...</p>
<p>Will you <a href="/donate">make a donation</a> to help us further our mission of creating beautiful, free ebooks?</p>
<p>It takes a team of highly-skilled volunteers hours to create and proof each of our ebooks. We couldnt do it without the financial support of literature lovers like you. Any amount helps further our mission!</p>
<p class="button-row center">
<a href="/donate" class="button">Make a donation towards free literature</a>
</p>
<p class="stinger">Join the Patrons Circle</p>
<p><a href="/donate#patrons-circle">Joining our Patrons Circle</a> gives you access to some awesome book-lover benefits:</p>
<ul>
<li>
<p>Your name <a href="/about#patrons-circle">listed on our masthead</a>. (You can also remain anonymous if you prefer.)</p>
</li>
<li>
<p>Access to our various <a href="/feeds">ebook feeds</a>:</p>
<ul>
<li>
<p>Browse and download from the entire Standard Ebooks catalog directly in your ereading app using our <a href="/feeds/opds">OPDS feed</a>.</p>
</li>
<li>
<p>Get notified of new ebooks in your news client with our <a href="/feeds/atom">Atom</a> or <a href="/feeds/rss">RSS</a> feeds.</p>
</li>
<li>
<p>Parse and process the feeds to use our ebooks in your personal software projects.</p>
</li>
</ul>
</li>
<li>
<p>Access to <a href="/bulk-downloads">bulk ebook downloads</a> to easily download whole collections of ebooks at once.</p>
</li>
<li>
<p>The ability to submit a book for inclusion on our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a>, once per quarter. (Submissions must conform to our <a href="/contribute/collections-policy">collections policy</a> and are subject to approval.)</p>
</li>
<li>
<p>The right to periodically vote on a selection from our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a> to choose an ebook for immediate production. The resulting ebook will be a permanent addition to our <a href="/ebooks">online catalog of free digital literature</a>.</p>
</li>
</ul>
<p class="button-row center">
<a href="/donate#patrons-circle" class="button">Join the S.E. Patrons Circle</a>
</p>
</div>
</div>
</main>
<?= Template::Footer() ?>

View file

@ -228,7 +228,7 @@ catch(Exceptions\EbookNotFoundException){
<meta property="schema:description" content="epub"/>
<meta property="schema:encodingFormat" content="application/epub+zip"/>
<p>
<span><a property="schema:contentUrl" href="<?= $ebook->EpubUrl ?>" class="epub" download="">Compatible epub</a></span> <span></span> <span>All devices and apps except Kindles and Kobos.</span>
<span><a property="schema:contentUrl" href="<?= $ebook->Url ?>/download?format=<?= EbookFormat::Epub->value ?>" class="epub">Compatible epub</a></span> <span></span> <span>All devices and apps except Kindles and Kobos.</span>
</p>
</li>
<? } ?>
@ -237,7 +237,7 @@ catch(Exceptions\EbookNotFoundException){
<li property="schema:encoding" typeof="schema:MediaObject">
<meta property="schema:encodingFormat" content="application/x-mobipocket-ebook"/>
<p>
<span><a property="schema:contentUrl" href="<?= $ebook->Azw3Url ?>" class="amazon" download=""><span property="schema:description">azw3</span></a></span> <span></span> <span>Kindle devices and apps.<? if($ebook->KindleCoverUrl !== null){ ?> Also download the <a href="<?= $ebook->KindleCoverUrl ?>">Kindle cover thumbnail</a> to see the cover in your Kindles library. You may be interested in our <a href="/help/how-to-use-our-ebooks#kindle-faq">Kindle FAQ</a>.<? }else{ ?> Also see our <a href="/how-to-use-our-ebooks#kindle-faq">Kindle FAQ</a>.<? } ?></span>
<span><a property="schema:contentUrl" href="<?= $ebook->Url ?>/download?format=<?= EbookFormat::Azw3->value ?>" class="amazon"><span property="schema:description">azw3</span></a></span> <span></span> <span>Kindle devices and apps.<? if($ebook->KindleCoverUrl !== null){ ?> Also download the <a href="<?= $ebook->KindleCoverUrl ?>">Kindle cover thumbnail</a> to see the cover in your Kindles library. You may be interested in our <a href="/help/how-to-use-our-ebooks#kindle-faq">Kindle FAQ</a>.<? }else{ ?> Also see our <a href="/how-to-use-our-ebooks#kindle-faq">Kindle FAQ</a>.<? } ?></span>
</p>
</li>
<? } ?>
@ -246,7 +246,7 @@ catch(Exceptions\EbookNotFoundException){
<li property="schema:encoding" typeof="schema:MediaObject">
<meta property="schema:encodingFormat" content="application/kepub+zip"/>
<p>
<span><a property="schema:contentUrl" href="<?= $ebook->KepubUrl ?>" class="kobo" download=""><span property="schema:description">kepub</span></a></span> <span></span> <span>Kobo devices and apps. You may also be interested in our <a href="/help/how-to-use-our-ebooks#kobo-faq">Kobo FAQ</a>.</span>
<span><a property="schema:contentUrl" href="<?= $ebook->Url ?>/download?format=<?= EbookFormat::Kepub->value ?>" class="kobo"><span property="schema:description">kepub</span></a></span> <span></span> <span>Kobo devices and apps. You may also be interested in our <a href="/help/how-to-use-our-ebooks#kobo-faq">Kobo FAQ</a>.</span>
</p>
</li>
<? } ?>
@ -255,7 +255,7 @@ catch(Exceptions\EbookNotFoundException){
<li property="schema:encoding" typeof="schema:MediaObject">
<meta property="schema:encodingFormat" content="application/epub+zip"/>
<p>
<span><a property="schema:contentUrl" href="<?= $ebook->AdvancedEpubUrl ?>" class="epub" download=""><span property="schema:description">Advanced epub</span></a></span> <span></span> <span>An advanced format that uses the latest technology not yet fully supported by most ereaders.</span>
<span><a property="schema:contentUrl" href="<?= $ebook->Url ?>/download?format=<?= EbookFormat::AdvancedEpub->value ?>" class="epub"><span property="schema:description">Advanced epub</span></a></span> <span></span> <span>An advanced format that uses the latest technology not yet fully supported by most ereaders.</span>
</p>
</li>
<? } ?>

18
www/ebooks/opensearch.php Normal file
View file

@ -0,0 +1,18 @@
<?
header('Content-Type: text/xml; charset=utf-8');
print('<?xml version="1.0" encoding="utf-8"?>');
?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Standard Ebooks</ShortName>
<Description>Search the Standard Ebooks catalog.</Description>
<Developer>Standard Ebooks</Developer>
<Language>en-US</Language>
<SyndicationRight>open</SyndicationRight>
<OutputEncoding>UTF-8</OutputEncoding>
<InputEncoding>UTF-8</InputEncoding>
<Url type="application/xhtml+xml" template="<?= SITE_URL ?>/ebooks?query={searchTerms}&amp;per-page={count}&amp;page={startPage}"/>
<Url type="application/rss+xml" template="<?= SITE_URL ?>/feeds/rss/all?query={searchTerms}"/>
<Url type="application/atom+xml" template="<?= SITE_URL ?>/feeds/atom/all?query={searchTerms}"/>
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="<?= SITE_URL ?>/feeds/opds/all?query={searchTerms}"/>
<Query role="example" searchTerms="fiction" startPage="1" count="12"/>
</OpenSearchDescription>