Add password login option for some users, and further refinements to artwork management system

This commit is contained in:
Alex Cabal 2024-01-06 19:10:52 -06:00
parent 8a1b11b815
commit 5a1c05d8c5
22 changed files with 234 additions and 150 deletions

View file

@ -4,6 +4,7 @@ CREATE TABLE `Users` (
`Name` varchar(255) DEFAULT NULL,
`Created` datetime NOT NULL,
`Uuid` char(36) NOT NULL,
`PasswordHash` varchar(255) NULL,
PRIMARY KEY (`UserId`),
UNIQUE KEY `idxEmail` (`Email`)
) ENGINE=InnoDB AUTO_INCREMENT=281 DEFAULT CHARSET=utf8mb4;

View file

@ -17,7 +17,7 @@ use function Safe\sprintf;
* @property string $ImageUrl
* @property string $ThumbUrl
* @property string $Thumb2xUrl
* @property string $ImageSize
* @property string $Dimensions
* @property Ebook $Ebook
* @property Museum $Museum
* @property ?ImageMimeType $MimeType
@ -48,7 +48,7 @@ class Artwork extends PropertiesBase{
protected $_ImageUrl = null;
protected $_ThumbUrl = null;
protected $_Thumb2xUrl = null;
protected $_ImageSize = null;
protected $_Dimensions = null;
protected $_Ebook = null;
protected $_Museum = null;
protected ?ImageMimeType $_MimeType = null;
@ -157,26 +157,20 @@ class Artwork extends PropertiesBase{
return $this->_Thumb2xUrl;
}
protected function GetImageSize(): string{
protected function GetDimensions(): string{
try{
$bytes = @filesize(WEB_ROOT . $this->ImageUrl);
$sizes = 'BKMGTP';
$factor = intval(floor((strlen((string)$bytes) - 1) / 3));
$sizeNumber = sprintf('%.1f', $bytes / pow(1024, $factor));
$sizeUnit = $sizes[$factor] ?? '';
$this->_ImageSize = $sizeNumber . $sizeUnit;
list($imageWidth, $imageHeight) = getimagesize(WEB_ROOT . $this->ImageUrl);
if($imageWidth && $imageHeight){
$this->_ImageSize .= ' (' . $imageWidth . ' × ' . $imageHeight . ')';
$this->_Dimensions .= $imageWidth . ' × ' . $imageHeight;
}
}
catch(Exception){
// Image doesn't exist
$this->_ImageSize = '';
$this->_Dimensions = '';
}
return $this->_ImageSize;
return $this->_Dimensions;
}
protected function GetEbook(): ?Ebook{
@ -217,7 +211,7 @@ class Artwork extends PropertiesBase{
$error->Add($ex);
}
if(trim($this->Exception) == ''){
if($this->Exception !== null && trim($this->Exception) == ''){
$this->Exception = null;
}
@ -517,11 +511,6 @@ class Artwork extends PropertiesBase{
return false;
}
public function HasMatchingMuseum(): bool{
return true;
}
// ***********
// ORM METHODS
// ***********

View file

@ -38,7 +38,14 @@ if($GLOBALS['User'] === null){
$session = new Session();
try{
$session->Create($httpBasicAuthLogin);
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
if($password == ''){
$password = null;
}
// Most patrons have a null password, meaning they only need to log in using an email and a blank password.
// Some users with admin rights need a password to log in.
$session->Create($httpBasicAuthLogin, $password);
$GLOBALS['User'] = $session->User;
}
catch(Exception){

View file

@ -2,5 +2,5 @@
namespace Exceptions;
class InvalidUserException extends AppException{
protected $message = 'We couldnt locate you in our system.';
protected $message = 'We couldnt validate your login information.';
}

View file

@ -0,0 +1,5 @@
<?
namespace Exceptions;
class PasswordRequiredException extends AppException{
}

View file

@ -167,8 +167,8 @@ class Library{
private static function GetBrowsableArtwork(): array{
return Db::Query('
SELECT *
FROM Artworks
WHERE Status IN ("approved", "in_use")', [], 'Artwork');
from Artworks
where Status in ("approved", "in_use")', [], 'Artwork');
}
/**
@ -262,7 +262,6 @@ class Library{
}
return $matches;
}
/**

View file

@ -32,8 +32,8 @@ class Session extends PropertiesBase{
// METHODS
// *******
public function Create(?string $email = null): void{
$this->User = User::GetIfRegistered($email);
public function Create(?string $email = null, ?string $password = null): void{
$this->User = User::GetIfRegistered($email, $password);
$this->UserId = $this->User->UserId;
$existingSessions = Db::Query('
@ -59,7 +59,7 @@ class Session extends PropertiesBase{
', [$this->UserId, $this->SessionId, $this->Created]);
}
$this->SetSessionCookie($this->SessionId);
self::SetSessionCookie($this->SessionId);
}
public static function GetLoggedInUser(): ?User{

View file

@ -12,6 +12,7 @@ class User extends PropertiesBase{
public $Email;
public $Created;
public $Uuid;
public $PasswordHash;
protected $_IsRegistered = null;
protected $_Payments = null;
protected $_Benefits = null;
@ -73,19 +74,25 @@ class User extends PropertiesBase{
// METHODS
// *******
public function Create(): void{
public function Create(?string $password = null): void{
$uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString();
$this->Created = new DateTime();
$this->PasswordHash = null;
if($password !== null){
$this->PasswordHash = password_hash($password, PASSWORD_DEFAULT);
}
try{
Db::Query('
INSERT into Users (Email, Name, Uuid, Created)
INSERT into Users (Email, Name, Uuid, Created, PasswordHash)
values (?,
?,
?,
?,
?)
', [$this->Email, $this->Name, $this->Uuid, $this->Created]);
', [$this->Email, $this->Name, $this->Uuid, $this->Created, $this->PasswordHash]);
}
catch(PDOException $ex){
if(($ex->errorInfo[1] ?? 0) == 1062){
@ -137,7 +144,7 @@ class User extends PropertiesBase{
return $result[0];
}
public static function GetIfRegistered(?string $identifier): User{
public static function GetIfRegistered(?string $identifier, ?string $password = null): User{
// We consider a user "registered" if they have a row in the Benefits table.
// Emails without that row may only be signed up for the newsletter and thus are not "registered" users
// The identifier is either an email or a UUID (api key)
@ -157,6 +164,15 @@ class User extends PropertiesBase{
throw new Exceptions\InvalidUserException();
}
if($result[0]->PasswordHash !== null && $password === null){
// Indicate that a password is required before we log in
throw new Exceptions\PasswordRequiredException();
}
if($result[0]->PasswordHash !== null && !password_verify($password ?? '', $result[0]->PasswordHash)){
throw new Exceptions\InvalidUserException();
}
return $result[0];
}
}

View file

@ -112,6 +112,7 @@ if($artwork === null){
}
$artwork = new Artwork();
$artwork->Artist = new Artist();
$artwork->Artist = $artist;
$artwork->Name = $artworkName;
$artwork->CompletedYear = $completedYear;

View file

@ -1,5 +1,6 @@
<?
$artwork = $artwork ?? null;
$showCopyrightNotice = $showCopyrightNotice ?? true;
if($artwork === null){
return;
@ -15,31 +16,29 @@ if($artwork === null){
</picture>
</a>
<?= Template::ImageCopyrightNotice() ?>
<? if($showCopyrightNotice){ ?>
<?= Template::ImageCopyrightNotice() ?>
<? } ?>
<h2>Metadata</h2>
<table class="artwork-metadata">
<tr>
<td>Title</td>
<td><?= Formatter::ToPlainText($artwork->Name) ?></td>
<td><i><?= Formatter::ToPlainText($artwork->Name) ?></i></td>
</tr>
<tr>
<td>Artist</td>
<td>
<?= Formatter::ToPlainText($artwork->Artist->Name) ?><? if(sizeof($artwork->Artist->AlternateSpellings) > 0){ ?> (<abbr>AKA</abbr> <span class="author" typeof="schema:Person" property="schema:name"><?= implode('</span>, <span class="author" typeof="schema:Person" property="schema:name">', array_map('Formatter::ToPlainText', $artwork->Artist->AlternateSpellings)) ?></span>)<? } ?><? if($artwork->Artist->DeathYear !== null){ ?>, <abbr title="deceased">d.</abbr> <?= $artwork->Artist->DeathYear ?><? } ?>
<?= Formatter::ToPlainText($artwork->Artist->Name) ?><? if(sizeof($artwork->Artist->AlternateSpellings) > 0){ ?> (<abbr>AKA</abbr> <span class="author" typeof="schema:Person" property="schema:name"><?= implode('</span>, <span class="author" typeof="schema:Person" property="schema:name">', array_map('Formatter::ToPlainText', $artwork->Artist->AlternateSpellings)) ?></span>)<? } ?><? if($artwork->Artist->DeathYear !== null){ ?> (<abbr>d.</abbr> <?= $artwork->Artist->DeathYear ?>)<? } ?>
</td>
</tr>
<tr>
<td>Year completed</td>
<td><? if($artwork->CompletedYear === null){ ?>(unknown)<? }else{ ?><?= $artwork->CompletedYear ?><? if($artwork->CompletedYearIsCirca){ ?> (circa)<? } ?><? } ?></td>
<td><? if($artwork->CompletedYear === null){ ?>Unknown<? }else{ ?><? if($artwork->CompletedYearIsCirca){ ?>Circa <? } ?><?= $artwork->CompletedYear ?><? } ?></td>
</tr>
<tr>
<td>File size</td>
<td><?= $artwork->ImageSize ?></td>
</tr>
<tr>
<td>Uploaded</td>
<td><?= $artwork->Created->format('F j, Y g:i a') ?></td>
<td>Dimensions</td>
<td><?= $artwork->Dimensions ?></td>
</tr>
<tr>
<td>Status</td>
@ -70,9 +69,8 @@ if($artwork === null){
<h3>Page scans</h3>
<ul>
<li>Year book was published: <? if($artwork->PublicationYear !== null){ ?><?= $artwork->PublicationYear ?><? }else{ ?><i>Not provided</i><? } ?></li>
<li>Was book published in the U.S.: <? if($artwork->IsPublishedInUs){ ?>Yes<? }else{ ?>No<? } ?></li>
<li>Page scan of book publication year: <? if($artwork->PublicationYearPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->PublicationYearPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
<li>Page scan of rights statement page: <? if($artwork->CopyrightPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->CopyrightPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
<li>Page scan of rights statement: <? if($artwork->CopyrightPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->CopyrightPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
<li>Page scan of artwork: <? if($artwork->ArtworkPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->ArtworkPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
</ul>
<? } ?>

View file

@ -10,7 +10,7 @@ $useAdminUrl = $useAdminUrl ?? false;
<? }else{ ?>
<? $url = $artwork->Url; ?>
<? } ?>
<li class="<?= $artwork->Status ?>">
<li class="<?= str_replace('_', '-', $artwork->Status) ?>">
<div class="thumbnail-container">
<a href="<?= $url ?>">
<picture>
@ -25,8 +25,8 @@ $useAdminUrl = $useAdminUrl ?? false;
<? if(sizeof($artwork->Artist->AlternateSpellings) > 0){ ?>(<abbr>AKA</abbr> <span class="author" typeof="schema:Person" property="schema:name"><?= implode('</span>, <span class="author" typeof="schema:Person" property="schema:name">', array_map('Formatter::ToPlainText', $artwork->Artist->AlternateSpellings)) ?></span>)<? } ?>
</p>
<div>
<p>Year completed: <? if($artwork->CompletedYear === null){ ?>(unknown)<? }else{ ?><?= $artwork->CompletedYear ?><? if($artwork->CompletedYearIsCirca){ ?> (circa)<? } ?><? } ?></p>
<p>Status: <?= Template::ArtworkStatus(['artwork' => $artwork]) ?></p>
<p>Year completed: <? if($artwork->CompletedYear === null){ ?>Unknown<? }else{ ?><? if($artwork->CompletedYearIsCirca){ ?>Circa<? } ?><?= $artwork->CompletedYear ?><? } ?></p>
<? if($artwork->Status == COVER_ARTWORK_STATUS_IN_USE){ ?><p>Status: <?= Template::ArtworkStatus(['artwork' => $artwork]) ?></p><? } ?>
<? if(count($artwork->Tags) > 0){ ?>
<p>Tags:</p>
<ul class="tags"><? foreach($artwork->Tags as $tag){ ?><li><a href="<?= $tag->Url ?>"><?= Formatter::ToPlainText($tag->Name) ?></a></li><? } ?></ul>

View file

@ -1,7 +1,7 @@
<form action="/artworks" method="get" rel="search">
<label>Status
<select name="status" size="1">
<option value="all">All</option>
<option value="all"<? if($status === null){ ?> selected="selected"<? } ?>>All</option>
<option value="<?= COVER_ARTWORK_STATUS_APPROVED ?>"<? if($status == COVER_ARTWORK_STATUS_APPROVED){ ?> selected="selected"<? } ?>>Approved</option>
<option value="<?= COVER_ARTWORK_STATUS_IN_USE ?>"<? if($status == COVER_ARTWORK_STATUS_IN_USE){ ?> selected="selected"<? } ?>>In use</option>
</select>
@ -13,9 +13,9 @@
<span>Sort</span>
<span>
<select name="sort">
<option value="<?= SORT_COVER_ARTWORK_CREATED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_CREATED_NEWEST){ ?> selected="selected"<? } ?>>Added date (new &#x2192; old)</option>
<option value="<?= SORT_COVER_ARTWORK_CREATED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_CREATED_NEWEST){ ?> selected="selected"<? } ?>>Date added (new &#x2192; old)</option>
<option value="<?= SORT_COVER_ARTIST_ALPHA ?>"<? if($sort == SORT_COVER_ARTIST_ALPHA){ ?> selected="selected"<? } ?>>Artist name (a &#x2192; z)</option>
<option value="<?= SORT_COVER_ARTWORK_COMPLETED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_COMPLETED_NEWEST){ ?> selected="selected"<? } ?>>Completed date (new &#x2192; old)</option>
<option value="<?= SORT_COVER_ARTWORK_COMPLETED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_COMPLETED_NEWEST){ ?> selected="selected"<? } ?>>Date of artwork completion (new &#x2192; old)</option>
</select>
</span>
</label>

View file

@ -1,10 +1,9 @@
<?
$artwork = $artwork ?? null;
?>
<? if($artwork !== null){ ?>
<? if($artwork->Status === COVER_ARTWORK_STATUS_APPROVED){ ?>Approved<? } ?>
<? if($artwork->Status === COVER_ARTWORK_STATUS_DECLINED){ ?>Declined<? } ?>
<? if($artwork->Status === COVER_ARTWORK_STATUS_UNVERIFIED){ ?>Unverified<? } ?>
<? if($artwork->Status === COVER_ARTWORK_STATUS_IN_USE){ ?>In use<? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?> by <a href="<?= $artwork->Ebook->Url ?>" property="schema:url"><span property="schema:name"><?= Formatter::ToPlainText($artwork->Ebook->Title) ?></span></a><? } ?><? } ?>
<? if($artwork->Status === COVER_ARTWORK_STATUS_IN_USE){ ?>In use<? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?> for <i><a href="<?= $artwork->Ebook->Url ?>"><?= Formatter::ToPlainText($artwork->Ebook->Title) ?></a></i><? } ?><? } ?>
<? } ?>

View file

@ -1 +1 @@
<p>These images are thought to be free of copyright restrictions in the United States. They may still be under copyright in other countries. If youre not located in the United States, you must check your local laws to verify that these images are free of copyright restrictions in the country youre located in before accessing, downloading, or using them.</p>
<p>Careful research leads Standard Ebooks to believe that these images are free of copyright restrictions in the United States. You must do your own research to confirm their copyright status before using them. They may also still be under copyright in other countries. If youre not located in the United States, you must check your local laws to verify that these images are free of copyright restrictions in the country youre located in before accessing, downloading, or using them.</p>

View file

@ -29,7 +29,7 @@ catch(Exceptions\AppException){
<main class="artworks">
<?= Template::Error(['exception' => $exception]) ?>
<section class="narrow">
<?= Template::ArtworkDetail(['artwork' => $artwork]) ?>
<?= Template::ArtworkDetail(['artwork' => $artwork, 'showCopyrightNotice' => false]) ?>
<? if($artwork->Status == COVER_ARTWORK_STATUS_DECLINED){ ?>
<h2>Status</h2>
<p>This artwork has been declined by a reviewer.</p>

View file

@ -58,13 +58,11 @@ if($perPage !== COVER_ARTWORK_PER_PAGE){
$queryString .= '&amp;per-page=' . urlencode((string)$perPage);
}
$queryString = preg_replace('/^&amp;/ius', '', $queryString);
?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription]) ?>
<main class="artworks">
<section class="narrow">
<h1>Browse U.S. Public Domain Artwork</h1>
<p>You can help Standard Ebooks by <a href="/artworks/new">submitting new public domain artwork</a> to add to this catalog for use in future ebooks. For free access to the submission form, <a href="/about#editor-in-chief">contact the Editor-in-Chief</a>.</p>
<?= Template::ArtworkSearchForm(['query' => $query, 'status' => $status, 'sort' => $sort, 'perPage' => $perPage]) ?>
<?= Template::ImageCopyrightNotice() ?>
<? if($totalArtworkCount == 0){ ?>
@ -74,13 +72,13 @@ $queryString = preg_replace('/^&amp;/ius', '', $queryString);
<? } ?>
<? if($totalArtworkCount > 0){ ?>
<nav>
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<ol>
<? for($i = 1; $i < $pages + 1; $i++){ ?>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>"><?= $i ?></a></li>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>"><?= $i ?></a></li>
<? } ?>
</ol>
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
</nav>
<? } ?>
</section>

View file

@ -74,7 +74,7 @@ catch(Exceptions\InvalidPermissionsException){
<span>For existing artists, leave the year of death blank.</span>
<datalist id="artist-names">
<? foreach(Library::GetAllArtists() as $existingArtist){ ?>
<option value="<?= Formatter::ToPlainText($existingArtist->Name) ?>"><?= Formatter::ToPlainText($existingArtist->Name) ?>, d. <? if($existingArtist->DeathYear !== null){ ?><?= $existingArtist->DeathYear ?><? }else{ ?>(unknown)<? } ?></option>
<option value="<?= Formatter::ToPlainText($existingArtist->Name) ?>"><?= Formatter::ToPlainText($existingArtist->Name) ?>, d. <? if($existingArtist->DeathYear !== null){ ?><?= $existingArtist->DeathYear ?><? }else{ ?>unknown<? } ?></option>
<? } ?>
</datalist>
<input
@ -136,7 +136,7 @@ catch(Exceptions\InvalidPermissionsException){
/>
</label>
<label>
<span>Image</span>
<span>High-resolution image</span>
<span>jpg, bmp, png, and tiff are accepted.</span>
<input
type="file"
@ -175,7 +175,7 @@ catch(Exceptions\InvalidPermissionsException){
</label>
<label>
<span>URL of page with year of publication</span>
<span>Roman numerals are OK.</span>
<span>Roman numerals on the page scan are OK.</span>
<input
type="url"
name="artwork-publication-year-page-url"
@ -185,7 +185,7 @@ catch(Exceptions\InvalidPermissionsException){
</label>
<label>
<span>URL of page with rights statement</span>
<span>Might be same URL as above; non-English is OK; keywords in other languages include <i>droits</i> and <i>rechte vorbehalten</i>.</span>
<span>Might be same URL as above; non-English is OK; keywords in other languages include <i>droits</i> and <i>rechte vorbehalten</i>.</span>
<input
type="url"
name="artwork-copyright-page-url"
@ -212,7 +212,7 @@ catch(Exceptions\InvalidPermissionsException){
/>
</label>
</fieldset>
<p><strong>or</strong> a special reason for an exception:</p>
<p><strong>or</strong> a reason for a special exception:</p>
<fieldset>
<label>
<span>Exception reason</span>

View file

@ -128,8 +128,8 @@ ol.artwork-list.list > li{
padding: 1rem;
}
ol.artwork-list.list > li.in_use{
background-color: rgba(0, 0, 0, .2);
ol.artwork-list.list > li.in-use{
opacity: .5;
background-image: url("/images/stripes.svg");
}
@ -142,6 +142,12 @@ ol.artwork-list.list > li .thumbnail-container{
margin-left: auto;
}
ol.artwork-list.list > li .thumbnail-container a:has(img){
display: block;
line-height: 0;
border-radius: .25rem;
}
ol.artwork-list.list > li p{
text-align: left;
}
@ -209,13 +215,16 @@ main.artworks nav ol li a:hover{
text-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
}
.artworks img{
max-width: 100%;
}
.artworks form[action="/artworks"]{
align-items: end;
display: grid;
grid-gap: 1rem;
grid-template-columns: auto auto auto auto 1fr;
margin: 0 1rem;
margin-bottom: 4rem;
margin-top: 2rem;
max-width: calc(100% - 2rem);
}
@ -312,11 +321,12 @@ main.artworks nav ol li.highlighted:nth-last-child(2)::after{
.artwork-metadata td:first-child{
font-weight: bold;
width: 25%;
text-align: right;
white-space: nowrap;
}
.artwork-metadata td:last-child{
width: 75%;
width: 100%;
}
.artwork-metadata td{
@ -325,6 +335,7 @@ main.artworks nav ol li.highlighted:nth-last-child(2)::after{
.artworks h1 + a{
width: auto;
line-height: 0;
}
.artworks aside.tip{
@ -362,74 +373,6 @@ main.artworks nav ol li.highlighted:nth-last-child(2)::after{
justify-content: unset;
}
@media(max-width: 730px){
main.artwork nav ol li:not(.highlighted){
display: none;
}
}
@media(max-width: 680px){
main.artworks nav ol li.highlighted::before,
main.artworks nav ol li.highlighted::after{
display: none;
}
}
@media(max-width: 500px){
main.artworks nav{
margin-top: .5rem;
}
main.artworks nav > a{
margin-bottom: 0;
font-size: 0;
}
main.artworks nav > a::before,
main.artworks nav > a::after{
font-size: 1rem;
margin: 0 !important;
}
main.artworks nav a[rel]::before,
main.artworks nav a[rel]::after{
font-size: 1rem;
margin: 0;
}
}
@media(max-width: 400px){
/* Artist details */
form[action="/artworks"] > fieldset:nth-of-type(1){
grid-template-columns: 1fr;
}
form[action="/artworks"] > fieldset:nth-of-type(2){
grid-template-columns: 1fr;
}
form[action="/artworks"] > fieldset label:has(input[name="artwork-tags"]),
form[action="/artworks"] > fieldset label:has(input[name="artwork-image"]){
grid-column: 1;
}
}
@media(max-width: 380px){
main.artworks nav > a{
padding: 1rem;
}
}
@media(prefers-reduced-motion: reduce){
.artworks nav > a:last-child::after,
.artworks nav > a:last-child[href]:hover::after,
.artworks nav > a:first-child:before,
.artworks nav > a:first-child[href]:hover::before,
.artworks nav > a:last-child[href]:hover::after{
transition: none;
}
}
.artworks figure p{
background: #333;
border: 1px solid var(--border);
@ -494,3 +437,84 @@ main.artworks nav ol li.highlighted:nth-last-child(2)::after{
font-family: "Fira Mono", monospace;
font-size: .8rem;
}
@media(max-width: 730px){
main.artwork nav ol li:not(.highlighted){
display: none;
}
}
@media(max-width: 680px){
main.artworks nav ol li.highlighted::before,
main.artworks nav ol li.highlighted::after{
display: none;
}
}
@media(max-width: 500px){
main.artworks nav{
margin-top: .5rem;
}
main.artworks nav > a{
margin-bottom: 0;
font-size: 0;
}
main.artworks nav > a::before,
main.artworks nav > a::after{
font-size: 1rem;
margin: 0 !important;
}
main.artworks nav a[rel]::before,
main.artworks nav a[rel]::after{
font-size: 1rem;
margin: 0;
}
.artwork-metadata td{
display: block;
padding: 0;
}
.artwork-metadata td:first-child{
text-align: left;
}
.artwork-metadata tr + tr td:first-child{
padding-top: 1rem;
}
}
@media(max-width: 400px){
/* Artist details */
form[action="/artworks"] > fieldset:nth-of-type(1){
grid-template-columns: 1fr;
}
form[action="/artworks"] > fieldset:nth-of-type(2){
grid-template-columns: 1fr;
}
form[action="/artworks"] > fieldset label:has(input[name="artwork-tags"]),
form[action="/artworks"] > fieldset label:has(input[name="artwork-image"]){
grid-column: 1;
}
}
@media(max-width: 380px){
main.artworks nav > a{
padding: 1rem;
}
}
@media(prefers-reduced-motion: reduce){
.artworks nav > a:last-child::after,
.artworks nav > a:last-child[href]:hover::after,
.artworks nav > a:first-child:before,
.artworks nav > a:first-child[href]:hover::before,
.artworks nav > a:last-child[href]:hover::after{
transition: none;
}
}

View file

@ -821,6 +821,7 @@ h2 + .download-list tr.year-header:first-child th{
.ebooks nav li.highlighted a:focus,
a.button:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="text"]:focus,
input[type="month"]:focus,
input[type="number"]:focus,
@ -1714,6 +1715,7 @@ input[type="month"],
input[type="number"],
input[type="url"],
input[type="email"],
input[type="password"],
input[type="search"]{
-webkit-appearance: none;
appearance: none;
@ -1733,7 +1735,9 @@ input[type="search"]{
}
label.email input,
label.search input{
label.search input,
label.captcha input,
label:has(input[type="password"]) input{
padding-left: 2.5rem;
}
@ -1816,16 +1820,20 @@ label.select:hover > span + span::after{
label.select > span + span,
label.email,
label.search{
label.search,
label.captcha,
label:has(input[type="password"]){
position: relative;
max-width: 100%;
}
label.email::before,
label.search::before{
label.search::before,
label.captcha::before,
label:has(input[type="password"])::before{
display: block;
position: absolute;
top: calc(2rem + .7rem);
bottom: 1.2rem;
left: 1rem;
font-family: "Fork Awesome";
font-size: 1rem;
@ -1843,10 +1851,18 @@ label.search::before{
content: "\f002";
}
label.email::before{
label:has(input[type="email"])::before{
content: "\f0e0";
}
label:has(input[type="password"])::before{
content: "\f084";
}
label.captcha::before{
content: "\f256";
}
nav li.highlighted a,
nav a[rel],
a.button,
@ -1856,6 +1872,7 @@ input[type="month"],
input[type="number"],
input[type="url"],
input[type="email"],
input[type="password"],
input[type="search"],
select{
transition: border-color .5s, background-color .5s;
@ -1878,6 +1895,8 @@ input[type="url"]:focus,
input[type="url"]:hover,
input[type="email"]:focus,
input[type="email"]:hover,
input[type="password"]:focus,
input[type="password"]:hover,
input[type="search"]:focus,
input[type="search"]:hover,
textarea:focus,
@ -1894,6 +1913,7 @@ input[type="month"]:user-invalid,
input[type="number"]:user-invalid,
input[type="url"]:user-invalid,
input[type="email"]:user-invalid,
input[type="password"]:user-invalid,
input[type="search"]:user-invalid{
border-color: #ff0000;
box-shadow: 1px 1px 0 #ff0000, -1px -1px 0 #ff0000;
@ -1904,6 +1924,7 @@ input[type="month"]:-moz-ui-invalid,
input[type="number"]:-moz-ui-invalid,
input[type="url"]:-moz-ui-invalid,
input[type="email"]:-moz-ui-invalid,
input[type="password"]:-moz-ui-invalid,
input[type="search"]:-moz-ui-invalid{
border-color: #ff0000;
box-shadow: 1px 1px 0 #ff0000, -1px -1px 0 #ff0000;
@ -1974,6 +1995,7 @@ form[action="/ebooks"] button{
form.single-row{
display: flex;
flex-direction: column;
gap: 1rem;
}
form.single-row label{
@ -3601,6 +3623,7 @@ ul.feed p{
input[type="number"],
input[type="url"],
input[type="email"],
input[type="password"],
input[type="search"],
select,
a.button:hover,
@ -3617,6 +3640,8 @@ ul.feed p{
input[type="url"]:hover,
input[type="email"]:focus,
input[type="email"]:hover,
input[type="password"]:focus,
input[type="password"]:hover,
input[type="search"]:focus,
input[type="search"]:hover,
select:focus,

Binary file not shown.

View file

@ -12,26 +12,47 @@ $email = HttpInput::Str(SESSION, 'email', false);
$redirect = HttpInput::Str(SESSION, 'redirect', false) ?? HttpInput::Str(GET, 'redirect', false);
$exception = $_SESSION['exception'] ?? null;
$passwordRequired = false;
http_response_code(401);
if($exception){
if(is_a($exception, 'Exceptions\PasswordRequiredException')){
// This login requires a password to proceed.
// Prompt the user for a password.
http_response_code(401);
$passwordRequired = true;
$exception = null; // Clear the exception so we don't show an error
}
else{
http_response_code(422);
}
session_unset();
}
?><?= Template::Header(['title' => 'Log In', 'highlight' => '', 'description' => 'Log in to your Standard Ebooks Patrons Circle account.']) ?>
<main>
<section class="narrow">
<h1>Log in</h1>
<?= Template::Error(['exception' => $exception]) ?>
<? if(!$passwordRequired){ ?>
<p>Enter your email address to log in to Standard Ebooks. Once youre logged in, your Patrons Circle benefits (like <a href="/polls">voting in our occasional polls</a> and access to our <a href="/bulk-downloads">bulk ebook downloads</a> and <a href="/feeds">ebook feeds</a>) will be available to you.</p>
<p>Anyone can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
<p><strong>Important:</strong> When making your donation, you must have selected either “List my name publicly” or “Dont list publicly, but reveal to project” on the donation form; otherwise, your email address isnt shared with us, and we cant include you in our login system.</p>
<?= Template::Error(['exception' => $exception]) ?>
<? } ?>
<form method="post" action="/sessions" class="single-row">
<input type="hidden" name="redirect" value="<?= Formatter::ToPlainText($redirect) ?>" />
<? if($passwordRequired){ ?>
<input type="hidden" name="email" value="<?= Formatter::ToPlainText($email) ?>" maxlength="80" required="required" />
<label class="password">
<span>Your password</span>
<span>Logging in as <?= Formatter::ToPlainText($email) ?>.</span>
<input type="password" name="password" value="" required="required" />
</label>
<? }else{ ?>
<label class="email">Your email address
<input type="email" name="email" value="<?= Formatter::ToPlainText($email) ?>" maxlength="80" required="required" />
</label>
<? } ?>
<button>Log in</button>
</form>
</section>

View file

@ -13,6 +13,7 @@ $requestType = HttpInput::RequestType();
$session = new Session();
$email = HttpInput::Str(POST, 'email', false);
$password = HttpInput::Str(POST, 'password', false);
$redirect = HttpInput::Str(POST, 'redirect', false);
try{
@ -20,7 +21,7 @@ try{
$redirect = '/';
}
$session->Create($email);
$session->Create($email, $password);
if($requestType == WEB){
http_response_code(303);