Add PropertyFromHttp trait and update codebase to use new pattern

This commit is contained in:
Alex Cabal 2024-11-10 22:23:43 -06:00
parent c35c47b793
commit acb30b897c
47 changed files with 851 additions and 527 deletions

View file

@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `Artworks` (
`CompletedYearIsCirca` boolean NOT NULL DEFAULT FALSE, `CompletedYearIsCirca` boolean NOT NULL DEFAULT FALSE,
`Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`Status` enum('unverified', 'approved', 'declined', 'in_use') DEFAULT 'unverified', `Status` enum('unverified', 'approved', 'declined', 'in_use') NOT NULL DEFAULT 'unverified',
`SubmitterUserId` int(10) unsigned NULL, `SubmitterUserId` int(10) unsigned NULL,
`ReviewerUserId` int(10) unsigned NULL, `ReviewerUserId` int(10) unsigned NULL,
`MuseumUrl` varchar(255) NULL, `MuseumUrl` varchar(255) NULL,

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS `EbookSources` ( CREATE TABLE IF NOT EXISTS `EbookSources` (
`EbookId` int(10) unsigned NOT NULL, `EbookId` int(10) unsigned NOT NULL,
`Type` enum('project_gutenberg', 'project_gutenberg_australia', 'project_gutenberg_canada', 'internet_archive', 'hathi_trust', 'wikisource', 'google_books', 'faded_page', 'other') DEFAULT 'other', `Type` enum('project_gutenberg', 'project_gutenberg_australia', 'project_gutenberg_canada', 'internet_archive', 'hathi_trust', 'wikisource', 'google_books', 'faded_page', 'other') NOT NULL DEFAULT 'other',
`Url` varchar(255) NOT NULL, `Url` varchar(255) NOT NULL,
`SortOrder` tinyint(3) unsigned NOT NULL, `SortOrder` tinyint(3) unsigned NOT NULL,
KEY `index1` (`EbookId`) KEY `index1` (`EbookId`)

View file

@ -9,49 +9,39 @@ use Safe\DateTimeImmutable;
*/ */
class Artist{ class Artist{
use Traits\Accessor; use Traits\Accessor;
use Traits\PropertyFromHttp;
public ?int $ArtistId = null; public int $ArtistId;
public ?string $Name = null; public string $Name = '';
public ?DateTimeImmutable $Created = null; public DateTimeImmutable $Created;
public ?DateTimeImmutable $Updated = null; public DateTimeImmutable $Updated;
public ?int $DeathYear = null;
protected ?int $_DeathYear = null; protected string $_UrlName;
protected ?string $_UrlName = null; protected string $_Url;
protected ?string $_Url = null; /** @var array<string> $_AlternateNames */
/** @var ?array<string> $_AlternateNames */ protected array $_AlternateNames;
protected $_AlternateNames = null;
// *******
// SETTERS
// *******
protected function SetDeathYear(?int $deathYear): void{
if($this->Name == 'Anonymous'){
$this->_DeathYear = null;
}
else {
$this->_DeathYear = $deathYear;
}
}
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetUrlName(): string{ protected function GetUrlName(): string{
if($this->Name === null || $this->Name == ''){ if(!isset($this->_UrlName)){
return ''; if(!isset($this->Name) || $this->Name == ''){
$this->_UrlName = '';
} }
else{
if($this->_UrlName === null){
$this->_UrlName = Formatter::MakeUrlSafe($this->Name); $this->_UrlName = Formatter::MakeUrlSafe($this->Name);
} }
}
return $this->_UrlName; return $this->_UrlName;
} }
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/artworks/' . $this->UrlName; $this->_Url = '/artworks/' . $this->UrlName;
} }
@ -62,7 +52,7 @@ class Artist{
* @return array<string> * @return array<string>
*/ */
protected function GetAlternateNames(): array{ protected function GetAlternateNames(): array{
if($this->_AlternateNames === null){ if(!isset($this->_AlternateNames)){
$this->_AlternateNames = []; $this->_AlternateNames = [];
$result = Db::Query(' $result = Db::Query('
@ -79,6 +69,7 @@ class Artist{
return $this->_AlternateNames; return $this->_AlternateNames;
} }
// ******* // *******
// METHODS // METHODS
// ******* // *******
@ -91,16 +82,15 @@ class Artist{
$error = new Exceptions\InvalidArtistException(); $error = new Exceptions\InvalidArtistException();
if($this->Name === null || $this->Name == ''){ if(!isset($this->Name) || $this->Name == ''){
$error->Add(new Exceptions\ArtistNameRequiredException()); $error->Add(new Exceptions\ArtistNameRequiredException());
} }
elseif(strlen($this->Name) > ARTWORK_MAX_STRING_LENGTH){
if($this->Name !== null && strlen($this->Name) > ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Artist Name')); $error->Add(new Exceptions\StringTooLongException('Artist Name'));
} }
if($this->Name == 'Anonymous' && $this->DeathYear !== null){ if($this->Name == 'Anonymous' && $this->DeathYear !== null){
$this->_DeathYear = null; $this->DeathYear = null;
} }
if($this->DeathYear !== null && ($this->DeathYear <= 0 || $this->DeathYear > $thisYear + 50)){ if($this->DeathYear !== null && ($this->DeathYear <= 0 || $this->DeathYear > $thisYear + 50)){
@ -111,11 +101,16 @@ class Artist{
throw $error; throw $error;
} }
} }
public function FillFromHttpPost(): void{
$this->PropertyFromHttp('Name');
$this->PropertyFromHttp('DeathYear');
}
// *********** // ***********
// ORM METHODS // ORM METHODS
// *********** // ***********
/** /**
* @throws Exceptions\ArtistNotFoundException * @throws Exceptions\ArtistNotFoundException
*/ */
@ -124,13 +119,11 @@ class Artist{
throw new Exceptions\ArtistNotFoundException(); throw new Exceptions\ArtistNotFoundException();
} }
$result = Db::Query(' return Db::Query('
SELECT * SELECT *
from Artists from Artists
where ArtistId = ? where ArtistId = ?
', [$artistId], Artist::class); ', [$artistId], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException();
return $result[0] ?? throw new Exceptions\ArtistNotFoundException();
} }
/** /**
@ -141,15 +134,13 @@ class Artist{
throw new Exceptions\ArtistNotFoundException(); throw new Exceptions\ArtistNotFoundException();
} }
$result = Db::Query(' return Db::Query('
SELECT a.* SELECT a.*
from Artists a from Artists a
left outer join ArtistAlternateNames aan using (ArtistId) left outer join ArtistAlternateNames aan using (ArtistId)
where aan.UrlName = ? where aan.UrlName = ?
limit 1 limit 1
', [$urlName], Artist::class); ', [$urlName], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException();
return $result[0] ?? throw new Exceptions\ArtistNotFoundException();
} }
/** /**

View file

@ -7,6 +7,7 @@ use function Safe\getimagesize;
use function Safe\parse_url; use function Safe\parse_url;
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\unlink;
/** /**
* @property string $UrlName * @property string $UrlName
@ -24,19 +25,20 @@ use function Safe\preg_replace;
* @property string $Dimensions * @property string $Dimensions
* @property Ebook $Ebook * @property Ebook $Ebook
* @property Museum $Museum * @property Museum $Museum
* @property User $Submitter * @property ?User $Submitter
* @property User $Reviewer * @property ?User $Reviewer
*/ */
class Artwork{ class Artwork{
use Traits\Accessor; use Traits\Accessor;
use Traits\PropertyFromHttp;
public ?string $Name = null; public int $ArtworkId;
public ?int $ArtworkId = null; public string $Name = '';
public ?int $ArtistId = null; public int $ArtistId;
public ?int $CompletedYear = null; public ?int $CompletedYear = null;
public bool $CompletedYearIsCirca = false; public bool $CompletedYearIsCirca = false;
public ?DateTimeImmutable $Created = null; public DateTimeImmutable $Created;
public ?DateTimeImmutable $Updated = null; public DateTimeImmutable $Updated;
public ?string $EbookUrl = null; public ?string $EbookUrl = null;
public ?int $SubmitterUserId = null; public ?int $SubmitterUserId = null;
public ?int $ReviewerUserId = null; public ?int $ReviewerUserId = null;
@ -45,26 +47,27 @@ class Artwork{
public ?string $PublicationYearPageUrl = null; public ?string $PublicationYearPageUrl = null;
public ?string $CopyrightPageUrl = null; public ?string $CopyrightPageUrl = null;
public ?string $ArtworkPageUrl = null; public ?string $ArtworkPageUrl = null;
public ?bool $IsPublishedInUs = null; public bool $IsPublishedInUs = false;
public ?string $Exception = null; public ?string $Exception = null;
public ?string $Notes = null; public ?string $Notes = null;
public ?Enums\ImageMimeType $MimeType = null; public Enums\ImageMimeType $MimeType;
public ?Enums\ArtworkStatusType $Status = null; public Enums\ArtworkStatusType $Status = Enums\ArtworkStatusType::Unverified;
protected string $_UrlName;
protected string $_Url;
protected string $_EditUrl;
/** @var array<ArtworkTag> $_Tags */
protected array $_Tags;
protected Artist $_Artist;
protected string $_ImageUrl;
protected string $_ThumbUrl;
protected string $_Thumb2xUrl;
protected string $_Dimensions ;
protected ?Ebook $_Ebook;
protected ?Museum $_Museum;
protected ?User $_Submitter;
protected ?User $_Reviewer;
protected ?string $_UrlName = null;
protected ?string $_Url = null;
protected ?string $_EditUrl = null;
/** @var ?array<ArtworkTag> $_Tags */
protected $_Tags = null;
protected ?Artist $_Artist = null;
protected ?string $_ImageUrl = null;
protected ?string $_ThumbUrl = null;
protected ?string $_Thumb2xUrl = null;
protected ?string $_Dimensions = null;
protected ?Ebook $_Ebook = null;
protected ?Museum $_Museum = null;
protected ?User $_Submitter = null;
protected ?User $_Reviewer = null;
// ******* // *******
// SETTERS // SETTERS
@ -74,45 +77,54 @@ class Artwork{
* @param string|null|array<ArtworkTag> $tags * @param string|null|array<ArtworkTag> $tags
*/ */
protected function SetTags(null|string|array $tags): void{ protected function SetTags(null|string|array $tags): void{
if($tags === null || is_array($tags)){ if(is_array($tags)){
$this->_Tags = $tags; $this->_Tags = $tags;
} }
elseif(is_string($tags)){ else{
$tags = trim($tags ?? '');
if($tags === ''){
$this->_Tags = [];
}
else{
$tags = array_map('trim', explode(',', $tags)); $tags = array_map('trim', explode(',', $tags));
$tags = array_values(array_filter($tags)); $tags = array_values(array_filter($tags));
$tags = array_unique($tags); $tags = array_unique($tags);
$this->_Tags = array_map(function ($str){ $this->_Tags = array_map(function ($str): ArtworkTag{
$tag = new ArtworkTag(); $tag = new ArtworkTag();
$tag->Name = $str; $tag->Name = $str;
return $tag; return $tag;
}, $tags); }, $tags);
} }
} }
}
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetUrlName(): string{ protected function GetUrlName(): string{
if($this->Name === null || $this->Name == ''){ if(!isset($this->_UrlName)){
return ''; if(!isset($this->Name) || $this->Name == ''){
$this->_UrlName = '';
} }
else{
if($this->_UrlName === null){
$this->_UrlName = Formatter::MakeUrlSafe($this->Name); $this->_UrlName = Formatter::MakeUrlSafe($this->Name);
} }
}
return $this->_UrlName; return $this->_UrlName;
} }
protected function GetSubmitter(): ?User{ protected function GetSubmitter(): ?User{
if($this->_Submitter === null){ if(!isset($this->_Submitter)){
try{ try{
$this->_Submitter = User::Get($this->SubmitterUserId); $this->_Submitter = User::Get($this->SubmitterUserId);
} }
catch(Exceptions\UserNotFoundException){ catch(Exceptions\UserNotFoundException){
// Return null $this->Submitter = null;
} }
} }
@ -120,12 +132,12 @@ class Artwork{
} }
protected function GetReviewer(): ?User{ protected function GetReviewer(): ?User{
if($this->_Reviewer === null){ if(!isset($this->_Reviewer)){
try{ try{
$this->_Reviewer = User::Get($this->ReviewerUserId); $this->_Reviewer = User::Get($this->ReviewerUserId);
} }
catch(Exceptions\UserNotFoundException){ catch(Exceptions\UserNotFoundException){
// Return null $this->_Reviewer = null;
} }
} }
@ -133,7 +145,7 @@ class Artwork{
} }
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/artworks/' . $this->Artist->UrlName . '/' . $this->UrlName; $this->_Url = '/artworks/' . $this->Artist->UrlName . '/' . $this->UrlName;
} }
@ -141,7 +153,7 @@ class Artwork{
} }
protected function GetEditUrl(): string{ protected function GetEditUrl(): string{
if($this->_EditUrl === null){ if(!isset($this->_EditUrl)){
$this->_EditUrl = $this->Url . '/edit'; $this->_EditUrl = $this->Url . '/edit';
} }
@ -152,7 +164,7 @@ class Artwork{
* @return array<ArtworkTag> * @return array<ArtworkTag>
*/ */
protected function GetTags(): array{ protected function GetTags(): array{
if($this->_Tags === null){ if(!isset($this->_Tags)){
$this->_Tags = Db::Query(' $this->_Tags = Db::Query('
SELECT t.* SELECT t.*
from Tags t from Tags t
@ -168,34 +180,28 @@ class Artwork{
* @throws Exceptions\InvalidUrlException * @throws Exceptions\InvalidUrlException
*/ */
public function GetMuseum(): ?Museum{ public function GetMuseum(): ?Museum{
if($this->_Museum === null){ if(!isset($this->_Museum)){
try{ try{
$this->_Museum = Museum::GetByUrl($this->MuseumUrl); $this->_Museum = Museum::GetByUrl($this->MuseumUrl);
} }
catch(Exceptions\MuseumNotFoundException){ catch(Exceptions\MuseumNotFoundException){
// Pass // Pass.
} }
} }
return $this->_Museum; return $this->_Museum;
} }
public function ImplodeTags(): string{
$tags = $this->Tags ?? [];
$tags = array_column($tags, 'Name');
return trim(implode(', ', $tags));
}
/** /**
* @throws Exceptions\InvalidArtworkException * @throws Exceptions\InvalidArtworkException
*/ */
protected function GetImageUrl(): string{ protected function GetImageUrl(): string{
if($this->_ImageUrl === null){ if(!isset($this->_ImageUrl)){
if($this->ArtworkId === null || $this->MimeType === null){ if(!isset($this->ArtworkId) || !isset($this->MimeType)){
throw new Exceptions\InvalidArtworkException(); throw new Exceptions\InvalidArtworkException();
} }
$this->_ImageUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . $this->MimeType->GetFileExtension() . '?ts=' . $this->Updated?->getTimestamp(); $this->_ImageUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . $this->MimeType->GetFileExtension() . '?ts=' . $this->Updated->getTimestamp();
} }
return $this->_ImageUrl; return $this->_ImageUrl;
@ -205,12 +211,12 @@ class Artwork{
* @throws Exceptions\ArtworkNotFoundException * @throws Exceptions\ArtworkNotFoundException
*/ */
protected function GetThumbUrl(): string{ protected function GetThumbUrl(): string{
if($this->_ThumbUrl === null){ if(!isset($this->_ThumbUrl)){
if($this->ArtworkId === null){ if(!isset($this->ArtworkId)){
throw new Exceptions\ArtworkNotFoundException(); throw new Exceptions\ArtworkNotFoundException();
} }
$this->_ThumbUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb.jpg' . '?ts=' . $this->Updated?->getTimestamp(); $this->_ThumbUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb.jpg' . '?ts=' . $this->Updated->getTimestamp();
} }
return $this->_ThumbUrl; return $this->_ThumbUrl;
@ -220,12 +226,12 @@ class Artwork{
* @throws Exceptions\ArtworkNotFoundException * @throws Exceptions\ArtworkNotFoundException
*/ */
protected function GetThumb2xUrl(): string{ protected function GetThumb2xUrl(): string{
if($this->_Thumb2xUrl === null){ if(!isset($this->_Thumb2xUrl)){
if($this->ArtworkId === null){ if(!isset($this->ArtworkId)){
throw new Exceptions\ArtworkNotFoundException(); throw new Exceptions\ArtworkNotFoundException();
} }
$this->_Thumb2xUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb@2x.jpg' . '?ts=' . $this->Updated?->getTimestamp(); $this->_Thumb2xUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb@2x.jpg' . '?ts=' . $this->Updated->getTimestamp();
} }
return $this->_Thumb2xUrl; return $this->_Thumb2xUrl;
@ -244,6 +250,7 @@ class Artwork{
} }
protected function GetDimensions(): string{ protected function GetDimensions(): string{
if(!isset($this->Dimensions)){
$this->_Dimensions = ''; $this->_Dimensions = '';
try{ try{
list($imageWidth, $imageHeight) = getimagesize($this->ImageFsPath); list($imageWidth, $imageHeight) = getimagesize($this->ImageFsPath);
@ -252,14 +259,15 @@ class Artwork{
} }
} }
catch(Exception){ catch(Exception){
// Image doesn't exist, return blank string // Image doesn't exist, return a blank string.
}
} }
return $this->_Dimensions; return $this->_Dimensions;
} }
protected function GetEbook(): ?Ebook{ protected function GetEbook(): ?Ebook{
if($this->_Ebook === null){ if(!isset($this->_Ebook)){
if($this->EbookUrl === null){ if($this->EbookUrl === null){
return null; return null;
} }
@ -277,9 +285,11 @@ class Artwork{
return $this->_Ebook; return $this->_Ebook;
} }
// ******* // *******
// METHODS // METHODS
// ******* // *******
public function CanBeEditedBy(?User $user): bool{ public function CanBeEditedBy(?User $user): bool{
if($user === null){ if($user === null){
return false; return false;
@ -336,16 +346,17 @@ class Artwork{
$thisYear = intval(NOW->format('Y')); $thisYear = intval(NOW->format('Y'));
$error = new Exceptions\InvalidArtworkException(); $error = new Exceptions\InvalidArtworkException();
if($this->Artist === null){ if(!isset($this->Artist)){
$error->Add(new Exceptions\InvalidArtistException()); $error->Add(new Exceptions\InvalidArtistException());
} }
else{
try{ try{
$this->Artist->Validate(); $this->Artist->Validate();
} }
catch(Exceptions\ValidationException $ex){ catch(Exceptions\ValidationException $ex){
$error->Add($ex); $error->Add($ex);
} }
}
if($this->Exception !== null && trim($this->Exception) == ''){ if($this->Exception !== null && trim($this->Exception) == ''){
$this->Exception = null; $this->Exception = null;
@ -355,13 +366,18 @@ class Artwork{
$this->Notes = null; $this->Notes = null;
} }
if($this->Name === null || $this->Name == ''){ if(isset($this->Name)){
if($this->Name == ''){
$error->Add(new Exceptions\ArtworkNameRequiredException()); $error->Add(new Exceptions\ArtworkNameRequiredException());
} }
if($this->Name !== null && strlen($this->Name) > ARTWORK_MAX_STRING_LENGTH){ if(strlen($this->Name) > ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Artwork Name')); $error->Add(new Exceptions\StringTooLongException('Artwork Name'));
} }
}
else{
$error->Add(new Exceptions\ArtworkNameRequiredException());
}
if($this->CompletedYear !== null && ($this->CompletedYear <= 0 || $this->CompletedYear > $thisYear)){ if($this->CompletedYear !== null && ($this->CompletedYear <= 0 || $this->CompletedYear > $thisYear)){
$error->Add(new Exceptions\InvalidCompletedYearException()); $error->Add(new Exceptions\InvalidCompletedYearException());
@ -375,10 +391,11 @@ class Artwork{
$error->Add(new Exceptions\InvalidPublicationYearException()); $error->Add(new Exceptions\InvalidPublicationYearException());
} }
if($this->Status === null){ if(!isset($this->Status)){
$error->Add(new Exceptions\InvalidArtworkException('Invalid status.')); $error->Add(new Exceptions\InvalidArtworkException('Invalid status.'));
} }
if(isset($this->Tags)){
if(count($this->Tags) == 0){ if(count($this->Tags) == 0){
$error->Add(new Exceptions\TagsRequiredException()); $error->Add(new Exceptions\TagsRequiredException());
} }
@ -395,6 +412,10 @@ class Artwork{
$error->Add($ex); $error->Add($ex);
} }
} }
}
else{
$error->Add(new Exceptions\TagsRequiredException());
}
if($this->MuseumUrl !== null){ if($this->MuseumUrl !== null){
if(strlen($this->MuseumUrl) > ARTWORK_MAX_STRING_LENGTH){ if(strlen($this->MuseumUrl) > ARTWORK_MAX_STRING_LENGTH){
@ -482,36 +503,38 @@ class Artwork{
} }
} }
// Check for existing Artwork objects with the same URL but different Artwork IDs. // Check for existing `Artwork` objects with the same URL but different `ArtworkID`s.
if(isset($this->ArtworkId)){
try{ try{
$existingArtwork = Artwork::GetByUrl($this->Artist->UrlName, $this->UrlName); $existingArtwork = Artwork::GetByUrl($this->Artist->UrlName, $this->UrlName);
if($existingArtwork->ArtworkId != $this->ArtworkId){ if($existingArtwork->ArtworkId != $this->ArtworkId){
// Duplicate found, alert the user // Duplicate found, alert the user.
$error->Add(new Exceptions\ArtworkAlreadyExistsException()); $error->Add(new Exceptions\ArtworkAlreadyExistsException());
} }
} }
catch(Exceptions\ArtworkNotFoundException){ catch(Exceptions\ArtworkNotFoundException){
// No duplicates found, continue // No duplicates found, continue.
}
} }
if($isImageRequired && $imagePath === null){ if($isImageRequired){
if($imagePath === null){
$error->Add(new Exceptions\InvalidImageUploadException('An image is required.')); $error->Add(new Exceptions\InvalidImageUploadException('An image is required.'));
} }
else{
if($imagePath !== null && $this->MimeType !== null){
if(!is_writable(WEB_ROOT . COVER_ART_UPLOAD_PATH)){ if(!is_writable(WEB_ROOT . COVER_ART_UPLOAD_PATH)){
$error->Add(new Exceptions\InvalidImageUploadException('Upload path not writable.')); $error->Add(new Exceptions\InvalidImageUploadException('Upload path not writable.'));
} }
// Check for minimum dimensions // Check for minimum dimensions.
list($imageWidth, $imageHeight) = getimagesize($imagePath); list($imageWidth, $imageHeight) = getimagesize($imagePath);
if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){ if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){
$error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException()); $error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException());
} }
} }
}
if($imagePath !== null && $this->MimeType === null && !$error->Has('Exceptions\InvalidImageUploadException')){ if(!isset($this->MimeType)){
// Only notify of wrong mimetype if there are no other problem with the uploaded image
$error->Add(new Exceptions\InvalidMimeTypeException()); $error->Add(new Exceptions\InvalidMimeTypeException());
} }
@ -520,6 +543,12 @@ class Artwork{
} }
} }
public function ImplodeTags(): string{
$tags = $this->Tags ?? [];
$tags = array_column($tags, 'Name');
return trim(implode(', ', $tags));
}
/** /**
* @throws Exceptions\InvalidUrlException * @throws Exceptions\InvalidUrlException
* @throws Exceptions\InvalidPageScanUrlException * @throws Exceptions\InvalidPageScanUrlException
@ -661,11 +690,12 @@ class Artwork{
* @throws Exceptions\InvalidImageUploadException * @throws Exceptions\InvalidImageUploadException
*/ */
public function Create(?string $imagePath = null): void{ public function Create(?string $imagePath = null): void{
$this->MimeType = Enums\ImageMimeType::FromFile($imagePath); $this->MimeType = Enums\ImageMimeType::FromFile($imagePath) ?? throw new Exceptions\InvalidImageUploadException();
$this->Validate($imagePath, true); $this->Validate($imagePath, true);
$this->Created = NOW; $this->Created = NOW;
$this->Updated = NOW;
$tags = []; $tags = [];
foreach($this->Tags as $artworkTag){ foreach($this->Tags as $artworkTag){
@ -677,7 +707,7 @@ class Artwork{
Db::Query(' Db::Query('
INSERT into INSERT into
Artworks (ArtistId, Name, UrlName, CompletedYear, CompletedYearIsCirca, Created, Status, SubmitterUserId, ReviewerUserId, MuseumUrl, Artworks (ArtistId, Name, UrlName, CompletedYear, CompletedYearIsCirca, Created, Updated, Status, SubmitterUserId, ReviewerUserId, MuseumUrl,
PublicationYear, PublicationYearPageUrl, CopyrightPageUrl, ArtworkPageUrl, IsPublishedInUs, PublicationYear, PublicationYearPageUrl, CopyrightPageUrl, ArtworkPageUrl, IsPublishedInUs,
EbookUrl, MimeType, Exception, Notes) EbookUrl, MimeType, Exception, Notes)
values (?, values (?,
@ -698,9 +728,10 @@ class Artwork{
?, ?,
?, ?,
?, ?,
?,
?) ?)
', [$this->Artist->ArtistId, $this->Name, $this->UrlName, $this->CompletedYear, $this->CompletedYearIsCirca, ', [$this->Artist->ArtistId, $this->Name, $this->UrlName, $this->CompletedYear, $this->CompletedYearIsCirca,
$this->Created, $this->Status, $this->SubmitterUserId, $this->ReviewerUserId, $this->MuseumUrl, $this->PublicationYear, $this->PublicationYearPageUrl, $this->Created, $this->Updated, $this->Status, $this->SubmitterUserId, $this->ReviewerUserId, $this->MuseumUrl, $this->PublicationYear, $this->PublicationYearPageUrl,
$this->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookUrl, $this->MimeType, $this->Exception, $this->Notes] $this->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookUrl, $this->MimeType, $this->Exception, $this->Notes]
); );
@ -726,16 +757,16 @@ class Artwork{
* @throws Exceptions\InvalidImageUploadException * @throws Exceptions\InvalidImageUploadException
*/ */
public function Save(?string $imagePath = null): void{ public function Save(?string $imagePath = null): void{
$this->_UrlName = null; unset($this->_UrlName);
if($imagePath !== null){ if($imagePath !== null){
$this->MimeType = Enums\ImageMimeType::FromFile($imagePath); $this->MimeType = Enums\ImageMimeType::FromFile($imagePath) ?? throw new Exceptions\InvalidImageUploadException();
// Manually set the updated timestamp, because if we only update the image and nothing else, the row's updated timestamp won't change automatically. // Manually set the updated timestamp, because if we only update the image and nothing else, the row's updated timestamp won't change automatically.
$this->Updated = NOW; $this->Updated = NOW;
$this->_ImageUrl = null; unset($this->_ImageUrl);
$this->_ThumbUrl = null; unset($this->_ThumbUrl);
$this->_Thumb2xUrl = null; unset($this->_Thumb2xUrl);
} }
$this->Validate($imagePath, false); $this->Validate($imagePath, false);
@ -828,7 +859,29 @@ class Artwork{
from Artworks from Artworks
where ArtworkId = ? where ArtworkId = ?
', [$this->ArtworkId]); ', [$this->ArtworkId]);
try{
unlink($this->ImageFsPath);
} }
catch(\Safe\Exceptions\FilesystemException){
// Pass.
}
try{
unlink($this->ThumbFsPath);
}
catch(\Safe\Exceptions\FilesystemException){
// Pass.
}
try{
unlink($this->Thumb2xFsPath);
}
catch(\Safe\Exceptions\FilesystemException){
// Pass.
}
}
// *********** // ***********
// ORM METHODS // ORM METHODS
@ -871,14 +924,10 @@ class Artwork{
public static function FromHttpPost(): Artwork{ public static function FromHttpPost(): Artwork{
$artwork = new Artwork(); $artwork = new Artwork();
$artwork->Artist = new Artist();
$artwork->Artist->Name = HttpInput::Str(POST, 'artist-name'); $artwork->Name = HttpInput::Str(POST, 'artwork-name') ?? '';
$artwork->Artist->DeathYear = HttpInput::Int(POST, 'artist-year-of-death');
$artwork->Name = HttpInput::Str(POST, 'artwork-name');
$artwork->CompletedYear = HttpInput::Int(POST, 'artwork-year'); $artwork->CompletedYear = HttpInput::Int(POST, 'artwork-year');
$artwork->CompletedYearIsCirca = HttpInput::Bool(POST, 'artwork-year-is-circa') ?? false; $artwork->CompletedYearIsCirca = HttpInput::Bool(POST, 'artwork-completed-year-is-circa') ?? false;
$artwork->Tags = HttpInput::Str(POST, 'artwork-tags') ?? []; $artwork->Tags = HttpInput::Str(POST, 'artwork-tags') ?? [];
$artwork->Status = Enums\ArtworkStatusType::tryFrom(HttpInput::Str(POST, 'artwork-status') ?? '') ?? Enums\ArtworkStatusType::Unverified; $artwork->Status = Enums\ArtworkStatusType::tryFrom(HttpInput::Str(POST, 'artwork-status') ?? '') ?? Enums\ArtworkStatusType::Unverified;
$artwork->EbookUrl = HttpInput::Str(POST, 'artwork-ebook-url'); $artwork->EbookUrl = HttpInput::Str(POST, 'artwork-ebook-url');
@ -893,4 +942,28 @@ class Artwork{
return $artwork; return $artwork;
} }
public function FillFromHttpPost(): void{
if(!isset($this->Artist)){
$this->Artist = new Artist();
}
$this->Artist->FillFromHttpPost();
$this->PropertyFromHttp('Name');
$this->PropertyFromHttp('CompletedYear');
$this->PropertyFromHttp('CompletedYearIsCirca');
$this->PropertyFromHttp('Status');
$this->PropertyFromHttp('EbookUrl');
$this->PropertyFromHttp('IsPublishedInUs');
$this->PropertyFromHttp('PublicationYear');
$this->PropertyFromHttp('PublicationYearPageUrl');
$this->PropertyFromHttp('CopyrightPageUrl');
$this->PropertyFromHttp('ArtworkPageUrl');
$this->PropertyFromHttp('MuseumUrl');
$this->PropertyFromHttp('Exception');
$this->PropertyFromHttp('Notes');
$this->Tags = HttpInput::Str(POST, 'artwork-tags') ?? ''; // Converted from a string to an array via a setter.
}
} }

View file

@ -7,18 +7,20 @@ class ArtworkTag extends Tag{
$this->Type = Enums\TagType::Artwork; $this->Type = Enums\TagType::Artwork;
} }
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/artworks?query=' . Formatter::MakeUrlSafe($this->Name); $this->_Url = '/artworks?query=' . Formatter::MakeUrlSafe($this->Name);
} }
return $this->_Url; return $this->_Url;
} }
// ******* // *******
// METHODS // METHODS
// ******* // *******
@ -29,6 +31,7 @@ class ArtworkTag extends Tag{
public function Validate(): void{ public function Validate(): void{
$error = new Exceptions\InvalidArtworkTagException($this->Name); $error = new Exceptions\InvalidArtworkTagException($this->Name);
if(isset($this->Name)){
$this->Name = mb_strtolower(trim($this->Name)); $this->Name = mb_strtolower(trim($this->Name));
// Collapse spaces into one // Collapse spaces into one
$this->Name = preg_replace('/[\s]+/ius', ' ', $this->Name); $this->Name = preg_replace('/[\s]+/ius', ' ', $this->Name);
@ -45,6 +48,12 @@ class ArtworkTag extends Tag{
$error->Add(new Exceptions\InvalidArtworkTagNameException()); $error->Add(new Exceptions\InvalidArtworkTagNameException());
} }
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
}
else{
$error->Add(new Exceptions\ArtworkTagNameRequiredException());
}
if($this->Type != Enums\TagType::Artwork){ if($this->Type != Enums\TagType::Artwork){
$error->Add(new Exceptions\InvalidArtworkTagTypeException($this->Type)); $error->Add(new Exceptions\InvalidArtworkTagTypeException($this->Type));
} }
@ -61,10 +70,11 @@ class ArtworkTag extends Tag{
$this->Validate(); $this->Validate();
Db::Query(' Db::Query('
INSERT into Tags (Name, Type) INSERT into Tags (Name, UrlName, Type)
values (?, values (?,
?,
?) ?)
', [$this->Name, $this->Type]); ', [$this->Name, $this->UrlName, $this->Type]);
$this->TagId = Db::GetLastInsertedId(); $this->TagId = Db::GetLastInsertedId();
} }

View file

@ -19,18 +19,19 @@ class AtomFeed extends Feed{
$this->Stylesheet = SITE_URL . '/feeds/atom/style'; $this->Stylesheet = SITE_URL . '/feeds/atom/style';
} }
// ******* // *******
// METHODS // METHODS
// ******* // *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ 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]); $feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'subtitle' => $this->Subtitle, 'updated' => $this->Updated, 'entries' => $this->Entries]);
$this->XmlString = $this->CleanXmlString($feed); $this->_XmlString = $this->CleanXmlString($feed);
} }
return $this->XmlString; return $this->_XmlString;
} }
public function SaveIfChanged(): bool{ public function SaveIfChanged(): bool{
@ -58,7 +59,7 @@ class AtomFeed extends Feed{
$obj->Id = SITE_URL . $entry->Url; $obj->Id = SITE_URL . $entry->Url;
} }
else{ else{
$obj->Updated = $entry->Updated !== null ? $entry->Updated->format(Enums\DateTimeFormat::Iso->value) : ''; $obj->Updated = $entry->Updated->format(Enums\DateTimeFormat::Iso->value);
$obj->Id = $entry->Id; $obj->Id = $entry->Id;
} }
$currentEntries[] = $obj; $currentEntries[] = $obj;

View file

@ -13,6 +13,11 @@ class Collection{
public ?Enums\CollectionType $Type = null; public ?Enums\CollectionType $Type = null;
protected ?string $_Url = null; protected ?string $_Url = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if($this->_Url === null){
$this->_Url = '/collections/' . $this->UrlName; $this->_Url = '/collections/' . $this->UrlName;
@ -21,6 +26,11 @@ class Collection{
return $this->_Url; return $this->_Url;
} }
// ***********
// ORM METHODS
// ***********
public static function FromName(string $name): Collection{ public static function FromName(string $name): Collection{
$instance = new Collection(); $instance = new Collection();
$instance->Name = $name; $instance->Name = $name;
@ -45,6 +55,11 @@ class Collection{
return $result[0] ?? throw new Exceptions\CollectionNotFoundException();; return $result[0] ?? throw new Exceptions\CollectionNotFoundException();;
} }
// *******
// METHODS
// *******
public function GetSortedName(): string{ public function GetSortedName(): string{
return preg_replace('/^(the|and|a|)\s/ius', '', $this->Name); return preg_replace('/^(the|and|a|)\s/ius', '', $this->Name);
} }

View file

@ -5,9 +5,10 @@
class CollectionMembership{ class CollectionMembership{
use Traits\Accessor; use Traits\Accessor;
public ?int $EbookId = null; public int $EbookId;
public ?int $CollectionId = null; public int $CollectionId;
public ?int $SequenceNumber = null; public ?int $SequenceNumber = null;
public ?int $SortOrder = null; public int $SortOrder;
protected ?Collection $_Collection = null;
protected Collection $_Collection;
} }

View file

@ -1,11 +1,8 @@
<? <?
use Safe\DateTimeImmutable;
use function Safe\preg_match; use function Safe\preg_match;
class Contributor{ class Contributor{
public ?int $EbookId = null; public int $EbookId;
public string $Name; public string $Name;
public string $UrlName; public string $UrlName;
public ?string $SortName = null; public ?string $SortName = null;
@ -13,19 +10,11 @@ class Contributor{
public ?string $MarcRole = null; public ?string $MarcRole = null;
public ?string $FullName = null; public ?string $FullName = null;
public ?string $NacoafUrl = null; public ?string $NacoafUrl = null;
public ?int $SortOrder = null; public int $SortOrder;
public static function FromProperties(string $name, string $sortName = null, string $fullName = null, string $wikipediaUrl = null, string $marcRole = null, string $nacoafUrl = null): Contributor{ // *******
$instance = new Contributor(); // METHODS
$instance->Name = str_replace('\'', '', $name); // *******
$instance->UrlName = Formatter::MakeUrlSafe($name);
$instance->SortName = $sortName;
$instance->FullName = $fullName;
$instance->WikipediaUrl = $wikipediaUrl;
$instance->MarcRole = $marcRole;
$instance->NacoafUrl = $nacoafUrl;
return $instance;
}
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException

View file

@ -18,15 +18,10 @@ class DonationDrive{
public function __construct(public string $Name, public DateTimeImmutable $Start, public DateTimeImmutable $End, public int $BaseTargetDonationCount, public int $StretchTargetDonationCount){ public function __construct(public string $Name, public DateTimeImmutable $Start, public DateTimeImmutable $End, public int $BaseTargetDonationCount, public int $StretchTargetDonationCount){
} }
public static function GetByIsRunning(): ?DonationDrive{
foreach(DONATION_DRIVE_DATES as $donationDrive){
if(NOW > $donationDrive->Start && NOW < $donationDrive->End){
return $donationDrive;
}
}
return null; // *******
} // GETTERS
// *******
protected function GetDonationCount(): int{ protected function GetDonationCount(): int{
if(!isset($this->_DonationCount)){ if(!isset($this->_DonationCount)){
@ -92,4 +87,19 @@ class DonationDrive{
return $this->_IsStretchEnabled; return $this->_IsStretchEnabled;
} }
// ***********
// ORM METHODS
// ***********
public static function GetByIsRunning(): ?DonationDrive{
foreach(DONATION_DRIVE_DATES as $donationDrive){
if(NOW > $donationDrive->Start && NOW < $donationDrive->End){
return $donationDrive;
}
}
return null;
}
} }

View file

@ -46,7 +46,7 @@ use function Safe\shell_exec;
class Ebook{ class Ebook{
use Traits\Accessor; use Traits\Accessor;
public ?int $EbookId = null; public int $EbookId;
public string $Identifier; public string $Identifier;
public string $WwwFilesystemPath; public string $WwwFilesystemPath;
public string $RepoFilesystemPath; public string $RepoFilesystemPath;
@ -56,12 +56,12 @@ class Ebook{
public ?string $KepubUrl = null; public ?string $KepubUrl = null;
public ?string $Azw3Url = null; public ?string $Azw3Url = null;
public ?string $DistCoverUrl = null; public ?string $DistCoverUrl = null;
public ?string $Title = null; public string $Title;
public ?string $FullTitle = null; public ?string $FullTitle = null;
public ?string $AlternateTitle = null; public ?string $AlternateTitle = null;
public ?string $Description = null; public string $Description;
public ?string $LongDescription = null; public string $LongDescription;
public ?string $Language = null; public string $Language;
public int $WordCount; public int $WordCount;
public float $ReadingEase; public float $ReadingEase;
public ?string $GitHubUrl = null; public ?string $GitHubUrl = null;
@ -71,47 +71,48 @@ class Ebook{
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public DateTimeImmutable $Updated; public DateTimeImmutable $Updated;
public ?int $TextSinglePageByteCount = null; public ?int $TextSinglePageByteCount = null;
/** @var array<GitCommit> $_GitCommits */ /** @var array<GitCommit> $_GitCommits */
protected $_GitCommits = null; protected array $_GitCommits;
/** @var array<EbookTag> $_Tags */ /** @var array<EbookTag> $_Tags */
protected $_Tags = null; protected array $_Tags;
/** @var array<LocSubject> $_LocSubjects */ /** @var array<LocSubject> $_LocSubjects */
protected $_LocSubjects = null; protected array $_LocSubjects;
/** @var array<CollectionMembership> $_CollectionMemberships */ /** @var array<CollectionMembership> $_CollectionMemberships */
protected $_CollectionMemberships = null; protected array $_CollectionMemberships;
/** @var array<EbookSource> $_Sources */ /** @var array<EbookSource> $_Sources */
protected $_Sources = null; protected array $_Sources;
/** @var array<Contributor> $_Authors */ /** @var array<Contributor> $_Authors */
protected $_Authors = null; protected array $_Authors;
/** @var array<Contributor> $_Illustrators */ /** @var array<Contributor> $_Illustrators */
protected $_Illustrators = null; protected array $_Illustrators;
/** @var array<Contributor> $_Translators */ /** @var array<Contributor> $_Translators */
protected $_Translators = null; protected array$_Translators;
/** @var array<Contributor> $_Contributors */ /** @var array<Contributor> $_Contributors */
protected $_Contributors = null; protected array $_Contributors;
/** @var ?array<string> $_TocEntries */ /** @var ?array<string> $_TocEntries */
protected $_TocEntries = null; // A list of non-Roman ToC entries ONLY IF the work has the 'se:is-a-collection' metadata element, null otherwise. protected ?array $_TocEntries = null; // A list of non-Roman ToC entries *only if* the work has the `se:is-a-collection` metadata element; `null` otherwise.
protected ?string $_Url = null; protected string $_Url;
protected ?bool $_HasDownloads = null; protected bool $_HasDownloads;
protected ?string $_UrlSafeIdentifier = null; protected string $_UrlSafeIdentifier;
protected ?string $_HeroImageUrl = null; protected string $_HeroImageUrl;
protected ?string $_HeroImageAvifUrl = null; protected string $_HeroImageAvifUrl;
protected ?string $_HeroImage2xUrl = null; protected string $_HeroImage2xUrl;
protected ?string $_HeroImage2xAvifUrl = null; protected string $_HeroImage2xAvifUrl;
protected ?string $_CoverImageUrl = null; protected string $_CoverImageUrl;
protected ?string $_CoverImageAvifUrl = null; protected string $_CoverImageAvifUrl;
protected ?string $_CoverImage2xUrl = null; protected string $_CoverImage2xUrl;
protected ?string $_CoverImage2xAvifUrl = null; protected string $_CoverImage2xAvifUrl;
protected ?string $_ReadingEaseDescription = null; protected string $_ReadingEaseDescription;
protected ?string $_ReadingTime = null; protected string $_ReadingTime;
protected ?string $_AuthorsHtml = null; protected string $_AuthorsHtml;
protected ?string $_AuthorsUrl = null; // This is a single URL even if there are multiple authors; for example, /ebooks/karl-marx_friedrich-engels/ protected string $_AuthorsUrl; // This is a single URL even if there are multiple authors; for example, `/ebooks/karl-marx_friedrich-engels/`.
protected ?string $_ContributorsHtml = null; protected string $_ContributorsHtml;
protected ?string $_TitleWithCreditsHtml = null; protected string $_TitleWithCreditsHtml;
protected ?string $_TextUrl = null; protected string $_TextUrl;
protected ?string $_TextSinglePageUrl = null; protected string $_TextSinglePageUrl;
protected ?string $_TextSinglePageSizeFormatted = null; protected string $_TextSinglePageSizeFormatted;
protected ?string $_IndexableText = null; protected string $_IndexableText;
// ******* // *******
// GETTERS // GETTERS
@ -121,7 +122,7 @@ class Ebook{
* @return array<GitCommit> * @return array<GitCommit>
*/ */
protected function GetGitCommits(): array{ protected function GetGitCommits(): array{
if($this->_GitCommits === null){ if(!isset($this->_GitCommits)){
$this->_GitCommits = Db::Query(' $this->_GitCommits = Db::Query('
SELECT * SELECT *
from GitCommits from GitCommits
@ -137,7 +138,7 @@ class Ebook{
* @return array<EbookTag> * @return array<EbookTag>
*/ */
protected function GetTags(): array{ protected function GetTags(): array{
if($this->_Tags === null){ if(!isset($this->_Tags)){
$this->_Tags = Db::Query(' $this->_Tags = Db::Query('
SELECT t.* SELECT t.*
from Tags t from Tags t
@ -154,7 +155,7 @@ class Ebook{
* @return array<LocSubject> * @return array<LocSubject>
*/ */
protected function GetLocSubjects(): array{ protected function GetLocSubjects(): array{
if($this->_LocSubjects === null){ if(!isset($this->_LocSubjects)){
$this->_LocSubjects = Db::Query(' $this->_LocSubjects = Db::Query('
SELECT l.* SELECT l.*
from LocSubjects l from LocSubjects l
@ -171,7 +172,7 @@ class Ebook{
* @return array<CollectionMembership> * @return array<CollectionMembership>
*/ */
protected function GetCollectionMemberships(): array{ protected function GetCollectionMemberships(): array{
if($this->_CollectionMemberships === null){ if(!isset($this->_CollectionMemberships)){
$this->_CollectionMemberships = Db::Query(' $this->_CollectionMemberships = Db::Query('
SELECT * SELECT *
from CollectionEbooks from CollectionEbooks
@ -187,7 +188,7 @@ class Ebook{
* @return array<EbookSource> * @return array<EbookSource>
*/ */
protected function GetSources(): array{ protected function GetSources(): array{
if($this->_Sources === null){ if(!isset($this->_Sources)){
$this->_Sources = Db::Query(' $this->_Sources = Db::Query('
SELECT * SELECT *
from EbookSources from EbookSources
@ -203,7 +204,7 @@ class Ebook{
* @return array<Contributor> * @return array<Contributor>
*/ */
protected function GetAuthors(): array{ protected function GetAuthors(): array{
if($this->_Authors === null){ if(!isset($this->_Authors)){
$this->_Authors = Db::Query(' $this->_Authors = Db::Query('
SELECT * SELECT *
from Contributors from Contributors
@ -220,7 +221,7 @@ class Ebook{
* @return array<Contributor> * @return array<Contributor>
*/ */
protected function GetIllustrators(): array{ protected function GetIllustrators(): array{
if($this->_Illustrators === null){ if(!isset($this->_Illustrators)){
$this->_Illustrators = Db::Query(' $this->_Illustrators = Db::Query('
SELECT * SELECT *
from Contributors from Contributors
@ -237,7 +238,7 @@ class Ebook{
* @return array<Contributor> * @return array<Contributor>
*/ */
protected function GetTranslators(): array{ protected function GetTranslators(): array{
if($this->_Translators === null){ if(!isset($this->_Translators)){
$this->_Translators = Db::Query(' $this->_Translators = Db::Query('
SELECT * SELECT *
from Contributors from Contributors
@ -254,7 +255,7 @@ class Ebook{
* @return array<Contributor> * @return array<Contributor>
*/ */
protected function GetContributors(): array{ protected function GetContributors(): array{
if($this->_Contributors === null){ if(!isset($this->_Contributors)){
$this->_Contributors = Db::Query(' $this->_Contributors = Db::Query('
SELECT * SELECT *
from Contributors from Contributors
@ -268,10 +269,10 @@ class Ebook{
} }
/** /**
* @return array<string> * @return ?array<string>
*/ */
protected function GetTocEntries(): array{ protected function GetTocEntries(): ?array{
if($this->_TocEntries === null){ if(!isset($this->_TocEntries)){
$this->_TocEntries = []; $this->_TocEntries = [];
$result = Db::Query(' $result = Db::Query('
@ -279,18 +280,22 @@ class Ebook{
from TocEntries from TocEntries
where EbookId = ? where EbookId = ?
order by SortOrder asc order by SortOrder asc
', [$this->EbookId], stdClass::class); ', [$this->EbookId]);
foreach($result as $row){ foreach($result as $row){
$this->_TocEntries[] = $row->TocEntry; $this->_TocEntries[] = $row->TocEntry;
} }
if(sizeof($this->_TocEntries) == 0){
$this->_TocEntries = null;
}
} }
return $this->_TocEntries; return $this->_TocEntries;
} }
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath); $this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath);
} }
@ -298,7 +303,7 @@ class Ebook{
} }
protected function GetHasDownloads(): bool{ protected function GetHasDownloads(): bool{
if($this->_HasDownloads === null){ if(!isset($this->_HasDownloads)){
$this->_HasDownloads = $this->EpubUrl || $this->AdvancedEpubUrl || $this->KepubUrl || $this->Azw3Url; $this->_HasDownloads = $this->EpubUrl || $this->AdvancedEpubUrl || $this->KepubUrl || $this->Azw3Url;
} }
@ -306,7 +311,7 @@ class Ebook{
} }
protected function GetUrlSafeIdentifier(): string{ protected function GetUrlSafeIdentifier(): string{
if($this->_UrlSafeIdentifier === null){ if(!isset($this->_UrlSafeIdentifier)){
$this->_UrlSafeIdentifier = str_replace(['url:https://standardebooks.org/ebooks/', '/'], ['', '_'], $this->Identifier); $this->_UrlSafeIdentifier = str_replace(['url:https://standardebooks.org/ebooks/', '/'], ['', '_'], $this->Identifier);
} }
@ -318,7 +323,7 @@ class Ebook{
} }
protected function GetHeroImageUrl(): string{ protected function GetHeroImageUrl(): string{
if($this->_HeroImageUrl === null){ if(!isset($this->_HeroImageUrl)){
$this->_HeroImageUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero.jpg'; $this->_HeroImageUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero.jpg';
} }
@ -326,7 +331,7 @@ class Ebook{
} }
protected function GetHeroImageAvifUrl(): ?string{ protected function GetHeroImageAvifUrl(): ?string{
if($this->_HeroImageAvifUrl === null){ if(!isset($this->_HeroImageAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero.avif')){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero.avif')){
$this->_HeroImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero.avif'; $this->_HeroImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero.avif';
} }
@ -336,7 +341,7 @@ class Ebook{
} }
protected function GetHeroImage2xUrl(): string{ protected function GetHeroImage2xUrl(): string{
if($this->_HeroImage2xUrl === null){ if(!isset($this->_HeroImage2xUrl)){
$this->_HeroImage2xUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero@2x.jpg'; $this->_HeroImage2xUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero@2x.jpg';
} }
@ -344,7 +349,7 @@ class Ebook{
} }
protected function GetHeroImage2xAvifUrl(): ?string{ protected function GetHeroImage2xAvifUrl(): ?string{
if($this->_HeroImage2xAvifUrl === null){ if(!isset($this->_HeroImage2xAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero@2x.avif')){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-hero@2x.avif')){
$this->_HeroImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero@2x.avif'; $this->_HeroImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-hero@2x.avif';
} }
@ -354,7 +359,7 @@ class Ebook{
} }
protected function GetCoverImageUrl(): string{ protected function GetCoverImageUrl(): string{
if($this->_CoverImageUrl === null){ if(!isset($this->_CoverImageUrl)){
$this->_CoverImageUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover.jpg'; $this->_CoverImageUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover.jpg';
} }
@ -362,7 +367,7 @@ class Ebook{
} }
protected function GetCoverImageAvifUrl(): ?string{ protected function GetCoverImageAvifUrl(): ?string{
if($this->_CoverImageAvifUrl === null){ if(!isset($this->_CoverImageAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover.avif')){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover.avif')){
$this->_CoverImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover.avif'; $this->_CoverImageAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover.avif';
} }
@ -372,7 +377,7 @@ class Ebook{
} }
protected function GetCoverImage2xUrl(): string{ protected function GetCoverImage2xUrl(): string{
if($this->_CoverImage2xUrl === null){ if(!isset($this->_CoverImage2xUrl)){
$this->_CoverImage2xUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover@2x.jpg'; $this->_CoverImage2xUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover@2x.jpg';
} }
@ -380,7 +385,7 @@ class Ebook{
} }
protected function GetCoverImage2xAvifUrl(): ?string{ protected function GetCoverImage2xAvifUrl(): ?string{
if($this->_CoverImage2xAvifUrl === null){ if(!isset($this->_CoverImage2xAvifUrl)){
if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover@2x.avif')){ if(file_exists(WEB_ROOT . '/images/covers/' . $this->UrlSafeIdentifier . '-cover@2x.avif')){
$this->_CoverImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover@2x.avif'; $this->_CoverImage2xAvifUrl = '/images/covers/' . $this->UrlSafeIdentifier . '-' . $this->GetLatestCommitHash() . '-cover@2x.avif';
} }
@ -390,7 +395,7 @@ class Ebook{
} }
protected function GetReadingEaseDescription(): string{ protected function GetReadingEaseDescription(): string{
if($this->_ReadingEaseDescription === null){ if(!isset($this->_ReadingEaseDescription)){
if($this->ReadingEase > 89){ if($this->ReadingEase > 89){
$this->_ReadingEaseDescription = 'very easy'; $this->_ReadingEaseDescription = 'very easy';
} }
@ -418,7 +423,7 @@ class Ebook{
} }
protected function GetReadingTime(): string{ protected function GetReadingTime(): string{
if($this->_ReadingTime === null){ if(!isset($this->_ReadingTime)){
$readingTime = ceil($this->WordCount / AVERAGE_READING_WORDS_PER_MINUTE); $readingTime = ceil($this->WordCount / AVERAGE_READING_WORDS_PER_MINUTE);
$this->_ReadingTime = (string)$readingTime; $this->_ReadingTime = (string)$readingTime;
@ -449,7 +454,7 @@ class Ebook{
} }
protected function GetAuthorsHtml(): string{ protected function GetAuthorsHtml(): string{
if($this->_AuthorsHtml === null){ if(!isset($this->_AuthorsHtml)){
$this->_AuthorsHtml = Ebook::GenerateContributorList($this->Authors, true); $this->_AuthorsHtml = Ebook::GenerateContributorList($this->Authors, true);
} }
@ -457,7 +462,7 @@ class Ebook{
} }
protected function GetAuthorsUrl(): string{ protected function GetAuthorsUrl(): string{
if($this->_AuthorsUrl === null){ if(!isset($this->_AuthorsUrl)){
$this->_AuthorsUrl = preg_replace('|url:https://standardebooks.org/ebooks/([^/]+)/.*|ius', '/ebooks/\1', $this->Identifier); $this->_AuthorsUrl = preg_replace('|url:https://standardebooks.org/ebooks/([^/]+)/.*|ius', '/ebooks/\1', $this->Identifier);
} }
@ -465,7 +470,7 @@ class Ebook{
} }
protected function GetContributorsHtml(): string{ protected function GetContributorsHtml(): string{
if($this->_ContributorsHtml === null){ if(!isset($this->_ContributorsHtml)){
$this->_ContributorsHtml = ''; $this->_ContributorsHtml = '';
if(sizeof($this->Contributors) > 0){ if(sizeof($this->Contributors) > 0){
$this->_ContributorsHtml .= ' with ' . Ebook::GenerateContributorList($this->Contributors, false) . ';'; $this->_ContributorsHtml .= ' with ' . Ebook::GenerateContributorList($this->Contributors, false) . ';';
@ -492,7 +497,7 @@ class Ebook{
} }
protected function GetTitleWithCreditsHtml(): string{ protected function GetTitleWithCreditsHtml(): string{
if($this->_TitleWithCreditsHtml === null){ if(!isset($this->_TitleWithCreditsHtml)){
$titleContributors = ''; $titleContributors = '';
if(sizeof($this->Contributors) > 0){ if(sizeof($this->Contributors) > 0){
$titleContributors .= '. With ' . Ebook::GenerateContributorList($this->Contributors, false); $titleContributors .= '. With ' . Ebook::GenerateContributorList($this->Contributors, false);
@ -513,7 +518,7 @@ class Ebook{
} }
protected function GetTextUrl(): string{ protected function GetTextUrl(): string{
if($this->_TextUrl === null){ if(!isset($this->_TextUrl)){
$this->_TextUrl = $this->Url . '/text'; $this->_TextUrl = $this->Url . '/text';
} }
@ -521,7 +526,7 @@ class Ebook{
} }
protected function GetTextSinglePageUrl(): string{ protected function GetTextSinglePageUrl(): string{
if($this->_TextSinglePageUrl === null){ if(!isset($this->_TextSinglePageUrl)){
$this->_TextSinglePageUrl = $this->Url . '/text/single-page'; $this->_TextSinglePageUrl = $this->Url . '/text/single-page';
} }
@ -529,7 +534,7 @@ class Ebook{
} }
protected function GetTextSinglePageSizeFormatted(): string{ protected function GetTextSinglePageSizeFormatted(): string{
if($this->_TextSinglePageSizeFormatted === null){ if(!isset($this->_TextSinglePageSizeFormatted)){
$bytes = $this->TextSinglePageByteCount; $bytes = $this->TextSinglePageByteCount;
$sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); $sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
@ -551,7 +556,7 @@ class Ebook{
} }
protected function GetIndexableText(): string{ protected function GetIndexableText(): string{
if($this->_IndexableText === null){ if(!isset($this->_IndexableText)){
$this->_IndexableText = $this->FullTitle ?? $this->Title; $this->_IndexableText = $this->FullTitle ?? $this->Title;
$this->_IndexableText .= ' ' . $this->AlternateTitle; $this->_IndexableText .= ' ' . $this->AlternateTitle;
@ -585,6 +590,11 @@ class Ebook{
return $this->_IndexableText; return $this->_IndexableText;
} }
// ***********
// ORM METHODS
// ***********
/** /**
* Construct an Ebook from a filesystem path. * Construct an Ebook from a filesystem path.
* *
@ -714,8 +724,8 @@ class Ebook{
$xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/'); $xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$ebook->Title = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:title')); $ebook->Title = trim(Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:title')) ?? '');
if($ebook->Title === null){ if($ebook->Title == ''){
throw new Exceptions\EbookParsingException('Invalid <dc:title> element.'); throw new Exceptions\EbookParsingException('Invalid <dc:title> element.');
} }
@ -808,14 +818,16 @@ class Ebook{
$fileAs = (string)$author; $fileAs = (string)$author;
} }
$authors[] = Contributor::FromProperties( $contributor = new Contributor();
(string)$author, $contributor->Name = (string)$author;
$fileAs, $contributor->UrlName = Formatter::MakeUrlSafe($contributor->Name);
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:name.person.full-name"][@refines="#' . $id . '"]')), $contributor->SortName = $fileAs;
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.encyclopedia.wikipedia"][@refines="#' . $id . '"]')), $contributor->FullName = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:name.person.full-name"][@refines="#' . $id . '"]'));
'aut', $contributor->WikipediaUrl = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.encyclopedia.wikipedia"][@refines="#' . $id . '"]'));
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.authority.nacoaf"][@refines="#' . $id . '"]')) $contributor->MarcRole = 'aut';
); $contributor->NacoafUrl = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.authority.nacoaf"][@refines="#' . $id . '"]'));
$authors[] = $contributor;
} }
if(sizeof($authors) == 0){ if(sizeof($authors) == 0){
throw new Exceptions\EbookParsingException('Invalid <dc:creator> element.'); throw new Exceptions\EbookParsingException('Invalid <dc:creator> element.');
@ -833,14 +845,14 @@ class Ebook{
} }
foreach($xml->xpath('/package/metadata/meta[ (@property="role" or @property="se:role") and @refines="#' . $id . '"]') ?: [] as $role){ foreach($xml->xpath('/package/metadata/meta[ (@property="role" or @property="se:role") and @refines="#' . $id . '"]') ?: [] as $role){
$c = Contributor::FromProperties( $c = new Contributor();
(string)$contributor, $c->Name = (string)$contributor;
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]')), $c->UrlName = Formatter::MakeUrlSafe($contributor->Name);
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:name.person.full-name"][@refines="#' . $id . '"]')), $c->SortName = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]'));
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.encyclopedia.wikipedia"][@refines="#' . $id . '"]')), $c->FullName = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:name.person.full-name"][@refines="#' . $id . '"]'));
$role, $c->WikipediaUrl = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.encyclopedia.wikipedia"][@refines="#' . $id . '"]'));
Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.authority.nacoaf"][@refines="#' . $id . '"]')) $c->MarcRole = $role;
); $c->NacoafUrl = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:url.authority.nacoaf"][@refines="#' . $id . '"]'));
// A display-sequence of 0 indicates that we don't want to process this contributor. // A display-sequence of 0 indicates that we don't want to process this contributor.
$displaySequence = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="display-seq"][@refines="#' . $id . '"]')); $displaySequence = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="display-seq"][@refines="#' . $id . '"]'));
@ -875,9 +887,9 @@ class Ebook{
$ebook->Contributors = $contributors; $ebook->Contributors = $contributors;
// Some basic data. // Some basic data.
$ebook->Description = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:description')); $ebook->Description = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:description')) ?? '';
$ebook->Language = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:language')); $ebook->LongDescription = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:long-description"]')) ?? '';
$ebook->LongDescription = Ebook::NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:long-description"]')); $ebook->Language = Ebook::NullIfEmpty($xml->xpath('/package/metadata/dc:language')) ?? '';
$wordCount = 0; $wordCount = 0;
$wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]'); $wordCountElement = $xml->xpath('/package/metadata/meta[@property="se:word-count"]');
@ -1048,7 +1060,6 @@ class Ebook{
} }
} }
$this->KepubUrl = trim($this->KepubUrl ?? ''); $this->KepubUrl = trim($this->KepubUrl ?? '');
if($this->KepubUrl == ''){ if($this->KepubUrl == ''){
$this->KepubUrl = null; $this->KepubUrl = null;
@ -1095,7 +1106,7 @@ class Ebook{
} }
if(isset($this->Title)){ if(isset($this->Title)){
$this->Title = trim($this->Title ?? ''); $this->Title = trim($this->Title);
if($this->Title == ''){ if($this->Title == ''){
$error->Add(new Exceptions\EbookTitleRequiredException()); $error->Add(new Exceptions\EbookTitleRequiredException());
@ -1128,7 +1139,7 @@ class Ebook{
} }
if(isset($this->Description)){ if(isset($this->Description)){
$this->Description = trim($this->Description ?? ''); $this->Description = trim($this->Description);
if($this->Description == ''){ if($this->Description == ''){
$error->Add(new Exceptions\EbookDescriptionRequiredException()); $error->Add(new Exceptions\EbookDescriptionRequiredException());
@ -1139,7 +1150,7 @@ class Ebook{
} }
if(isset($this->LongDescription)){ if(isset($this->LongDescription)){
$this->LongDescription = trim($this->LongDescription ?? ''); $this->LongDescription = trim($this->LongDescription);
if($this->LongDescription == ''){ if($this->LongDescription == ''){
$error->Add(new Exceptions\EbookLongDescriptionRequiredException()); $error->Add(new Exceptions\EbookLongDescriptionRequiredException());
@ -1150,7 +1161,7 @@ class Ebook{
} }
if(isset($this->Language)){ if(isset($this->Language)){
$this->Language = trim($this->Language ?? ''); $this->Language = trim($this->Language);
if($this->Language == ''){ if($this->Language == ''){
$error->Add(new Exceptions\EbookLanguageRequiredException()); $error->Add(new Exceptions\EbookLanguageRequiredException());
@ -1520,8 +1531,9 @@ class Ebook{
return $string; return $string;
} }
/** /**
* If the given list of elements has an element that is not `''`, return that value; otherwise, return `null`.
*
* @param array<SimpleXMLElement>|false|null $elements * @param array<SimpleXMLElement>|false|null $elements
*/ */
private static function NullIfEmpty($elements): ?string{ private static function NullIfEmpty($elements): ?string{
@ -1529,8 +1541,6 @@ class Ebook{
return null; return null;
} }
// Helper function when getting values from SimpleXml.
// Checks if the result is set, and returns the value if so; if the value is the empty string, return null.
if(isset($elements[0])){ if(isset($elements[0])){
$str = (string)$elements[0]; $str = (string)$elements[0];
if($str !== ''){ if($str !== ''){
@ -1541,31 +1551,6 @@ class Ebook{
return null; return null;
} }
// ***********
// ORM METHODS
// ***********
/**
* @throws Exceptions\EbookNotFoundException
*/
public static function GetByIdentifier(?string $identifier): Ebook{
if($identifier === null){
throw new Exceptions\EbookNotFoundException('Invalid identifier: ' . $identifier);
}
$result = Db::Query('
SELECT *
from Ebooks
where Identifier = ?
', [$identifier], Ebook::class);
if(sizeof($result) == 0){
throw new Exceptions\EbookNotFoundException('Invalid identifier: ' . $identifier);
}
return $result[0];
}
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
*/ */
@ -1843,4 +1828,29 @@ class Ebook{
} }
} }
} }
// ***********
// ORM METHODS
// ***********
/**
* @throws Exceptions\EbookNotFoundException
*/
public static function GetByIdentifier(?string $identifier): Ebook{
if($identifier === null){
throw new Exceptions\EbookNotFoundException('Invalid identifier: ' . $identifier);
}
$result = Db::Query('
SELECT *
from Ebooks
where Identifier = ?
', [$identifier], Ebook::class);
if(sizeof($result) == 0){
throw new Exceptions\EbookNotFoundException('Invalid identifier: ' . $identifier);
}
return $result[0];
}
} }

View file

@ -3,18 +3,20 @@
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
class EbookSource{ class EbookSource{
public ?int $EbookId = null; public int $EbookId;
public Enums\EbookSourceType $Type; public Enums\EbookSourceType $Type;
public string $Url; public string $Url;
public ?int $SortOrder = null; public int $SortOrder;
// *******
// METHODS
// *******
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
*/ */
public function Validate(): void{ public function Validate(): void{
/** @throws void */
$now = new DateTimeImmutable();
$error = new Exceptions\ValidationException(); $error = new Exceptions\ValidationException();
if(!isset($this->EbookId)){ if(!isset($this->EbookId)){

View file

@ -4,25 +4,20 @@ class EbookTag extends Tag{
$this->Type = Enums\TagType::Ebook; $this->Type = Enums\TagType::Ebook;
} }
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetUrlName(): string{
if($this->_UrlName === null){
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
}
return $this->_UrlName;
}
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/subjects/' . $this->UrlName; $this->_Url = '/subjects/' . $this->UrlName;
} }
return $this->_Url; return $this->_Url;
} }
// ******* // *******
// METHODS // METHODS
// ******* // *******
@ -43,6 +38,8 @@ class EbookTag extends Tag{
if(strlen($this->Name) > EBOOKS_MAX_STRING_LENGTH){ if(strlen($this->Name) > EBOOKS_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook tag: '. $this->Name)); $error->Add(new Exceptions\StringTooLongException('Ebook tag: '. $this->Name));
} }
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
} }
else{ else{
$error->Add(new Exceptions\EbookTagNameRequiredException()); $error->Add(new Exceptions\EbookTagNameRequiredException());
@ -72,6 +69,11 @@ class EbookTag extends Tag{
$this->TagId = Db::GetLastInsertedId(); $this->TagId = Db::GetLastInsertedId();
} }
// ***********
// ORM METHODS
// ***********
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
*/ */

View file

@ -0,0 +1,7 @@
<?
namespace Exceptions;
class ArtworkTagNameRequiredException extends AppException{
/** @var string $message */
protected $message = 'Tag name is required.';
}

View file

@ -14,8 +14,8 @@ abstract class Feed{
public $Entries = []; public $Entries = [];
public string $Path; public string $Path;
public ?string $Stylesheet = null; public ?string $Stylesheet = null;
protected ?string $XmlString = null; protected string $_XmlString;
public ?DateTimeImmutable $Updated = null; public DateTimeImmutable $Updated;
/** /**
* @param string $title * @param string $title
@ -31,6 +31,13 @@ abstract class Feed{
} }
// *******
// GETTERS
// *******
abstract protected function GetXmlString(): string;
// ******* // *******
// METHODS // METHODS
// ******* // *******
@ -52,11 +59,6 @@ abstract class Feed{
return $output; return $output;
} }
protected function GetXmlString(): string{
// Virtual function, meant to be implemented by subclass
return '';
}
public function Save(): void{ public function Save(): void{
$feed = $this->GetXmlString(); $feed = $this->GetXmlString();

View file

@ -2,11 +2,16 @@
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
class GitCommit{ class GitCommit{
public ?int $EbookId = null; public int $EbookId;
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public string $Message; public string $Message;
public string $Hash; public string $Hash;
// ***********
// ORM METHODS
// ***********
/** /**
* @throws Exceptions\InvalidGitCommitException * @throws Exceptions\InvalidGitCommitException
*/ */
@ -26,6 +31,11 @@ class GitCommit{
return $instance; return $instance;
} }
// *******
// METHODS
// *******
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
*/ */

View file

@ -62,7 +62,7 @@ class Image{
unlink($tempFilename); unlink($tempFilename);
} }
catch(Exception){ catch(Exception){
// Pass if file doesn't exist // Pass if file doesn't exist.
} }
} }

View file

@ -5,15 +5,13 @@ use function Safe\exec;
use function Safe\filemtime; use function Safe\filemtime;
use function Safe\filesize; use function Safe\filesize;
use function Safe\glob; use function Safe\glob;
use function Safe\ksort;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\preg_split; use function Safe\preg_split;
use function Safe\sprintf;
use function Safe\usort;
class Library{ class Library{
/** /**
* @param array<string> $tags * @param array<string> $tags
*
* @return array{ebooks: array<Ebook>, ebooksCount: int} * @return array{ebooks: array<Ebook>, ebooksCount: int}
*/ */
public static function FilterEbooks(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE): array{ public static function FilterEbooks(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE): array{
@ -102,11 +100,11 @@ class Library{
', [$urlPath], Ebook::class); ', [$urlPath], Ebook::class);
} }
else{ else{
// Multiple authors, e.g., karl-marx_friedrich-engels // Multiple authors, e.g., `karl-marx_friedrich-engels`.
$authors = explode('_', $urlPath); $authors = explode('_', $urlPath);
$params = $authors; $params = $authors;
$params[] = sizeof($authors); // The number of authors in the URL must match the number of Contributor records. $params[] = sizeof($authors); // The number of authors in the URL must match the number of `Contributor` records.
return Db::Query(' return Db::Query('
SELECT e.* SELECT e.*
@ -123,6 +121,7 @@ class Library{
/** /**
* @return array<Collection> * @return array<Collection>
*
* @throws Exceptions\AppException * @throws Exceptions\AppException
*/ */
public static function GetEbookCollections(): array{ public static function GetEbookCollections(): array{
@ -135,7 +134,10 @@ class Library{
if($collator === null){ if($collator === null){
throw new Exceptions\AppException('Couldn\'t create collator object when getting collections.'); throw new Exceptions\AppException('Couldn\'t create collator object when getting collections.');
} }
usort($collections, function($a, $b) use($collator){ return $collator->compare($a->GetSortedName(), $b->GetSortedName()); }); usort($collections, function(Collection $a, Collection $b) use($collator){
$result = $collator->compare($a->GetSortedName(), $b->GetSortedName());
return $result === false ? 0 : $result;
});
return $collections; return $collections;
} }
@ -344,6 +346,7 @@ class Library{
/** /**
* @return array<Artwork> * @return array<Artwork>
*
* @throws Exceptions\ArtistNotFoundException * @throws Exceptions\ArtistNotFoundException
*/ */
public static function GetArtworksByArtist(?string $artistUrlName, ?string $status, ?int $submitterUserId): array{ public static function GetArtworksByArtist(?string $artistUrlName, ?string $status, ?int $submitterUserId): array{
@ -451,7 +454,8 @@ class Library{
/** /**
* @param array<int, stdClass> $items * @param array<int, stdClass> $items
* @return array<string, array<int|string, array<int|string, mixed>>> *
* @return array<int, stdClass>
*/ */
private static function SortBulkDownloads(array $items): array{ private static function SortBulkDownloads(array $items): array{
// This sorts our items in a special order, epub first and advanced epub last // This sorts our items in a special order, epub first and advanced epub last
@ -481,7 +485,8 @@ class Library{
} }
/** /**
* @return array<string, array<int|string, array<int|string, stdClass>>> * @return array{'months': array<string, array<string, stdClass>>, 'subjects': array<stdClass>, 'collections': array<stdClass>, 'authors': array<stdClass>}
*
* @throws Exceptions\AppException * @throws Exceptions\AppException
*/ */
public static function RebuildBulkDownloadsCache(): array{ public static function RebuildBulkDownloadsCache(): array{
@ -489,6 +494,7 @@ class Library{
if($collator === null){ if($collator === null){
throw new Exceptions\AppException('Couldn\'t create collator object when rebuilding bulk download cache.'); throw new Exceptions\AppException('Couldn\'t create collator object when rebuilding bulk download cache.');
} }
/** @var array<string, array<string, stdClass>> $months */
$months = []; $months = [];
$subjects = []; $subjects = [];
$collections = []; $collections = [];
@ -509,6 +515,8 @@ class Library{
catch(\Exception){ catch(\Exception){
throw new Exceptions\AppException('Couldn\'t parse date on bulk download object.'); throw new Exceptions\AppException('Couldn\'t parse date on bulk download object.');
} }
/** @var string $year Required to satisfy PHPStan */
$year = $date->format('Y'); $year = $date->format('Y');
$month = $date->format('F'); $month = $date->format('F');
@ -533,7 +541,10 @@ class Library{
foreach(glob(WEB_ROOT . '/bulk-downloads/collections/*/', GLOB_NOSORT) as $dir){ foreach(glob(WEB_ROOT . '/bulk-downloads/collections/*/', GLOB_NOSORT) as $dir){
$collections[] = self::FillBulkDownloadObject($dir, 'collections', '/collections'); $collections[] = self::FillBulkDownloadObject($dir, 'collections', '/collections');
} }
usort($collections, function($a, $b) use($collator){ return $collator->compare($a->LabelSort, $b->LabelSort); }); usort($collections, function(stdClass $a, stdClass $b) use($collator): int{
$result = $collator->compare($a->LabelSort, $b->LabelSort);
return $result === false ? 0 : $result;
});
apcu_store('bulk-downloads-collections', $collections, 43200); // 12 hours apcu_store('bulk-downloads-collections', $collections, 43200); // 12 hours
@ -541,7 +552,10 @@ class Library{
foreach(glob(WEB_ROOT . '/bulk-downloads/authors/*/', GLOB_NOSORT) as $dir){ foreach(glob(WEB_ROOT . '/bulk-downloads/authors/*/', GLOB_NOSORT) as $dir){
$authors[] = self::FillBulkDownloadObject($dir, 'authors', '/ebooks'); $authors[] = self::FillBulkDownloadObject($dir, 'authors', '/ebooks');
} }
usort($authors, function($a, $b) use($collator){ return $collator->compare($a->LabelSort, $b->LabelSort); }); usort($authors, function(stdClass $a, stdClass $b) use($collator): int{
$result = $collator->compare($a->LabelSort, $b->LabelSort);
return $result === false ? 0 : $result;
});
apcu_store('bulk-downloads-authors', $authors, 43200); // 12 hours apcu_store('bulk-downloads-authors', $authors, 43200); // 12 hours
@ -549,7 +563,8 @@ class Library{
} }
/** /**
* @return array<string, array<int|string, array<int|string, mixed>>> * @return array<stdClass>
*
* @throws Exceptions\AppException * @throws Exceptions\AppException
*/ */
public static function RebuildFeedsCache(?string $returnType = null, ?string $returnClass = null): ?array{ public static function RebuildFeedsCache(?string $returnType = null, ?string $returnClass = null): ?array{
@ -584,7 +599,10 @@ class Library{
$feeds[] = $obj; $feeds[] = $obj;
} }
usort($feeds, function($a, $b) use($collator){ return $collator->compare($a->LabelSort, $b->LabelSort); }); usort($feeds, function(stdClass $a, stdClass $b) use($collator): int{
$result = $collator->compare($a->LabelSort, $b->LabelSort);
return $result === false ? 0 : $result;
});
if($type == $returnType && $class == $returnClass){ if($type == $returnType && $class == $returnClass){
$retval = $feeds; $retval = $feeds;

View file

@ -3,6 +3,10 @@ class LocSubject{
public int $LocSubjectId; public int $LocSubjectId;
public string $Name; public string $Name;
// *******
// METHODS
// *******
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
*/ */

View file

@ -1,5 +1,4 @@
<? <?
use Safe\DateTimeImmutable;
use function Safe\fopen; use function Safe\fopen;
use function Safe\fwrite; use function Safe\fwrite;
use function Safe\fclose; use function Safe\fclose;

View file

@ -1,9 +1,12 @@
<? <?
use function Safe\glob; use function Safe\glob;
use function Safe\preg_match_all; use function Safe\preg_match_all;
use function Safe\sort;
class Manual{ class Manual{
// *******
// METHODS
// *******
public static function GetLatestVersion(): string{ public static function GetLatestVersion(): string{
$dirs = glob(MANUAL_PATH . '/*', GLOB_ONLYDIR); $dirs = glob(MANUAL_PATH . '/*', GLOB_ONLYDIR);
sort($dirs); sort($dirs);

View file

@ -10,6 +10,11 @@ class Museum{
public string $Name; public string $Name;
public string $Domain; public string $Domain;
// *******
// METHODS
// *******
/** /**
* @throws Exceptions\InvalidUrlException * @throws Exceptions\InvalidUrlException
* @throws Exceptions\InvalidMuseumUrlException * @throws Exceptions\InvalidMuseumUrlException
@ -597,6 +602,10 @@ class Museum{
return $outputUrl; return $outputUrl;
} }
// ***********
// ORM METHODS
// ***********
/** /**
* @throws Exceptions\MuseumNotFoundException * @throws Exceptions\MuseumNotFoundException
* @throws Exceptions\InvalidUrlException * @throws Exceptions\InvalidUrlException

View file

@ -13,15 +13,17 @@ class NewsletterSubscription{
public bool $IsSubscribedToNewsletter = false; public bool $IsSubscribedToNewsletter = false;
public ?int $UserId = null; public ?int $UserId = null;
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
protected ?User $_User = null;
protected ?string $_Url = null; protected ?User $_User;
protected string $_Url;
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/newsletter/subscriptions/' . $this->User->Uuid; $this->_Url = '/newsletter/subscriptions/' . $this->User->Uuid;
} }

View file

@ -13,10 +13,10 @@ class OpdsAcquisitionFeed extends OpdsFeed{
// ******* // *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if(!isset($this->_XmlString)){
$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])); $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; return $this->_XmlString;
} }
} }

View file

@ -36,7 +36,7 @@ abstract class OpdsFeed extends AtomFeed{
$this->Updated = $updated; $this->Updated = $updated;
$this->XmlString = null; unset($this->_XmlString);
file_put_contents($this->Path, $this->GetXmlString()); file_put_contents($this->Path, $this->GetXmlString());
// Do we have any parents of our own to update? // Do we have any parents of our own to update?

View file

@ -6,12 +6,12 @@ class OpdsNavigationEntry{
public string $Url; public string $Url;
public string $Rel; public string $Rel;
public string $Type; public string $Type;
public ?DateTimeImmutable $Updated = null; public DateTimeImmutable $Updated;
public string $Description; public string $Description;
public string $Title; public string $Title;
public string $SortTitle; public string $SortTitle;
public function __construct(string $title, string $description, string $url, ?DateTimeImmutable $updated, string $rel, string $type){ public function __construct(string $title, string $description, string $url, DateTimeImmutable $updated, string $rel, string $type){
$this->Id = SITE_URL . $url; $this->Id = SITE_URL . $url;
$this->Url = $url; $this->Url = $url;
$this->Rel = $rel; $this->Rel = $rel;

View file

@ -38,10 +38,10 @@ class OpdsNavigationFeed extends OpdsFeed{
// ******* // *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if(!isset($this->_XmlString)){
$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])); $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; return $this->_XmlString;
} }
} }

View file

@ -7,14 +7,15 @@ use Safe\DateTimeImmutable;
class Patron{ class Patron{
use Traits\Accessor; use Traits\Accessor;
public ?int $UserId = null; public int $UserId;
public bool $IsAnonymous; public bool $IsAnonymous;
public ?string $AlternateName = null; public ?string $AlternateName = null;
public bool $IsSubscribedToEmails; public bool $IsSubscribedToEmails;
public ?DateTimeImmutable $Created = null; public DateTimeImmutable $Created;
public ?DateTimeImmutable $Ended = null; public ?DateTimeImmutable $Ended = null;
protected ?User $_User = null; protected User $_User;
// ******* // *******
// METHODS // METHODS

View file

@ -19,6 +19,11 @@ class Payment{
protected ?User $_User = null; protected ?User $_User = null;
// *******
// GETTERS
// *******
/** /**
* @throws Exceptions\UserNotFoundException * @throws Exceptions\UserNotFoundException
*/ */

View file

@ -1,6 +1,5 @@
<? <?
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
use function Safe\usort;
/** /**
* @property string $Url * @property string $Url
@ -18,12 +17,13 @@ class Poll{
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public DateTimeImmutable $Start; public DateTimeImmutable $Start;
public DateTimeImmutable $End; public DateTimeImmutable $End;
protected ?string $_Url = null;
/** @var ?array<PollItem> $_PollItems */ protected string $_Url;
protected $_PollItems = null; /** @var array<PollItem> $_PollItems */
/** @var ?array<PollItem> $_PollItemsByWinner */ protected array $_PollItems;
protected $_PollItemsByWinner = null; /** @var array<PollItem> $_PollItemsByWinner */
protected ?int $_VoteCount = null; protected array $_PollItemsByWinner;
protected int $_VoteCount;
// ******* // *******
@ -31,7 +31,7 @@ class Poll{
// ******* // *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/polls/' . $this->UrlName; $this->_Url = '/polls/' . $this->UrlName;
} }
@ -39,7 +39,7 @@ class Poll{
} }
protected function GetVoteCount(): int{ protected function GetVoteCount(): int{
if($this->_VoteCount === null){ if(!isset($this->_VoteCount)){
$this->_VoteCount = Db::QueryInt(' $this->_VoteCount = Db::QueryInt('
SELECT count(*) SELECT count(*)
from PollVotes pv from PollVotes pv
@ -55,7 +55,7 @@ class Poll{
* @return array<PollItem> * @return array<PollItem>
*/ */
protected function GetPollItems(): array{ protected function GetPollItems(): array{
if($this->_PollItems === null){ if(!isset($this->_PollItems)){
$this->_PollItems = Db::Query(' $this->_PollItems = Db::Query('
SELECT * SELECT *
from PollItems from PollItems
@ -71,11 +71,11 @@ class Poll{
* @return array<PollItem> * @return array<PollItem>
*/ */
protected function GetPollItemsByWinner(): array{ protected function GetPollItemsByWinner(): array{
if($this->_PollItemsByWinner === null){ if(!isset($this->_PollItemsByWinner)){
$this->_PollItemsByWinner = $this->PollItems; $this->_PollItemsByWinner = $this->PollItems;
usort($this->_PollItemsByWinner, function(PollItem $a, PollItem $b){ return $a->VoteCount <=> $b->VoteCount; }); usort($this->_PollItemsByWinner, function(PollItem $a, PollItem $b){ return $a->VoteCount <=> $b->VoteCount; });
$this->_PollItemsByWinner = array_reverse($this->_PollItemsByWinner ?? []); $this->_PollItemsByWinner = array_reverse($this->_PollItemsByWinner);
} }
return $this->_PollItemsByWinner; return $this->_PollItemsByWinner;
@ -87,7 +87,6 @@ class Poll{
// ******* // *******
public function IsActive(): bool{ public function IsActive(): bool{
/** @throws void */
if( ($this->Start !== null && $this->Start > NOW) || ($this->End !== null && $this->End < NOW)){ if( ($this->Start !== null && $this->Start > NOW) || ($this->End !== null && $this->End < NOW)){
return false; return false;
} }

View file

@ -1,7 +1,4 @@
<? <?
use Exceptions\PollItemNotFoundException;
/** /**
* @property int $VoteCount * @property int $VoteCount
* @property Poll $Poll * @property Poll $Poll
@ -13,8 +10,9 @@ class PollItem{
public int $PollId; public int $PollId;
public string $Name; public string $Name;
public string $Description; public string $Description;
protected ?int $_VoteCount = null;
protected ?Poll $_Poll = null; protected int $_VoteCount;
protected Poll $_Poll;
// ******* // *******
@ -22,7 +20,7 @@ class PollItem{
// ******* // *******
protected function GetVoteCount(): int{ protected function GetVoteCount(): int{
if($this->_VoteCount === null){ if(!isset($this->_VoteCount)){
$this->_VoteCount = Db::QueryInt(' $this->_VoteCount = Db::QueryInt('
SELECT count(*) SELECT count(*)
from PollVotes pv from PollVotes pv

View file

@ -8,14 +8,15 @@ use Safe\DateTimeImmutable;
*/ */
class PollVote{ class PollVote{
use Traits\Accessor; use Traits\Accessor;
use Traits\PropertyFromHttp;
public ?int $UserId = null; public int $UserId;
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public ?int $PollItemId = null; public int $PollItemId;
protected ?User $_User = null; protected User $_User;
protected ?PollItem $_PollItem = null; protected PollItem $_PollItem;
protected ?string $_Url = null; protected string $_Url;
// ******* // *******
@ -23,7 +24,7 @@ class PollVote{
// ******* // *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = $this->PollItem->Poll->Url . '/votes/' . $this->UserId; $this->_Url = $this->PollItem->Poll->Url . '/votes/' . $this->UserId;
} }
@ -41,26 +42,30 @@ class PollVote{
protected function Validate(): void{ protected function Validate(): void{
$error = new Exceptions\InvalidPollVoteException(); $error = new Exceptions\InvalidPollVoteException();
if($this->User === null){ if(!isset($this->UserId)){
$error->Add(new Exceptions\UserNotFoundException()); $error->Add(new Exceptions\UserNotFoundException());
} }
else{
try{
// Attempt to get the `User`.
User::Get($this->UserId);
}
catch(Exceptions\UserNotFoundException $ex){
$error->Add($ex);
}
}
if($this->PollItemId === null){ if(!isset($this->PollItemId)){
$error->Add(new Exceptions\PollItemRequiredException()); $error->Add(new Exceptions\PollItemRequiredException());
} }
else{ else{
if($this->PollItem === null){ try{
$error->Add(new Exceptions\PollNotFoundException());
}
else{
if($this->PollItem->Poll === null){
$error->Add(new Exceptions\PollNotFoundException());
}
else{
if(!$this->PollItem->Poll->IsActive()){ if(!$this->PollItem->Poll->IsActive()){
$error->Add(new Exceptions\PollClosedException()); $error->Add(new Exceptions\PollClosedException());
} }
} }
catch(Exceptions\PollItemNotFoundException | Exceptions\PollNotFoundException){
$error->Add(new Exceptions\PollNotFoundException());
} }
} }
@ -97,10 +102,8 @@ class PollVote{
$this->UserId = $this->User->UserId; $this->UserId = $this->User->UserId;
} }
catch(Exceptions\UserNotFoundException){ catch(Exceptions\UserNotFoundException){
// Can't validate patron email - do nothing for now, // Can't validate patron email - do nothing for now, this will be caught later when we validate the vote during creation.
// this will be caught later when we validate the vote during creation. // Save the email in the User object in case we want it later, for example prefilling the 'create' form after an error is returned.
// Save the email in the User object in case we want it later,
// for example prefilling the 'create' form after an error is returned.
$this->User = new User(); $this->User = new User();
$this->User->Email = $email; $this->User->Email = $email;
} }
@ -135,4 +138,8 @@ class PollVote{
return $result[0] ?? throw new Exceptions\PollVoteNotFoundException(); return $result[0] ?? throw new Exceptions\PollVoteNotFoundException();
} }
public function FillFromHttpPost(): void{
$this->PropertyFromHttp('PollItemId');
}
} }

View file

@ -21,13 +21,13 @@ class RssFeed extends Feed{
// ******* // *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if(!isset($this->_XmlString)){
$feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => NOW]); $feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => NOW]);
$this->XmlString = $this->CleanXmlString($feed); $this->_XmlString = $this->CleanXmlString($feed);
} }
return $this->XmlString; return $this->_XmlString;
} }
public function SaveIfChanged(): bool{ public function SaveIfChanged(): bool{

View file

@ -13,8 +13,8 @@ class Session{
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
public string $SessionId; public string $SessionId;
protected ?User $_User = null; protected User $_User;
public ?string $_Url = null; public string $_Url;
// ******* // *******
@ -22,7 +22,7 @@ class Session{
// ******* // *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->_Url === null){ if(!isset($this->_Url)){
$this->_Url = '/sessions/' . $this->SessionId; $this->_Url = '/sessions/' . $this->SessionId;
} }
@ -101,6 +101,11 @@ class Session{
setcookie('sessionid', $sessionId, ['expires' => intval((new DateTimeImmutable('+1 week'))->format(Enums\DateTimeFormat::UnixTimestamp->value)), 'path' => '/', 'domain' => SITE_DOMAIN, 'secure' => true, 'httponly' => false, 'samesite' => 'Lax']); // Expires in two weeks setcookie('sessionid', $sessionId, ['expires' => intval((new DateTimeImmutable('+1 week'))->format(Enums\DateTimeFormat::UnixTimestamp->value)), 'path' => '/', 'domain' => SITE_DOMAIN, 'secure' => true, 'httponly' => false, 'samesite' => 'Lax']); // Expires in two weeks
} }
// ***********
// ORM METHODS
// ***********
/** /**
* @throws Exceptions\SessionNotFoundException * @throws Exceptions\SessionNotFoundException
*/ */

View file

@ -1,14 +1,14 @@
<? <?
/** /**
* @property string $Url * @property string $Url
* @property string $UrlName
*/ */
class Tag{ class Tag{
use Traits\Accessor; use Traits\Accessor;
public int $TagId; public int $TagId;
public string $Name; public string $Name;
public string $UrlName;
public Enums\TagType $Type; public Enums\TagType $Type;
protected ?string $_UrlName = null;
protected ?string $_Url = null; protected string $_Url;
} }

View file

@ -117,6 +117,7 @@ trait Accessor{
* $t->User = new User(); * $t->User = new User();
* *
* isset($t->User); // true * isset($t->User); // true
* ```
* *
* @see <https://reddit.com/r/PHPhelp/comments/x2avqu/anyone_know_the_rules_behind_null_coalescing/> * @see <https://reddit.com/r/PHPhelp/comments/x2avqu/anyone_know_the_rules_behind_null_coalescing/>
*/ */

View file

@ -0,0 +1,154 @@
<?
namespace Traits;
use function Safe\preg_replace;
trait PropertyFromHttp{
/**
* Given the string name of a property, try to fill it from HTTP data (POST by default).
*
* This function will try to infer the type of the class property using reflection.
*
* - If a variable doesn't match a class property (either by name or by type), then the class property is unchanged.
*
* - If a variable matches a class property both by name and type, then the class property is set to the variable.
*
* - If the class property type is both nullable and not `string` (e.g., the class property is `?int`), then a matching but empty variable will set that class property to `null`; this includes `?string` class properties. (I.e., a matching variable of value `""` will set a `?string` class property to `null`.)
*
* For example, consider:
*
* ```
* class Test{
* use Traits\PropertyFromHttp;
*
* public int $Id;
* public string $Name;
* public ?string $Description;
* public ?int $ChapterNumber;
* }
* ```
*
* - POST `['test-foo' => 'bar']`:
*
* No changes
*
* - POST `['test-id' => '123']`:
*
* `$Id`: set to `123`
*
* - POST `['test-id' => '']`:
*
* `$Id`: unchanged, because it is not nullable
*
* - POST `['test-name' => 'bob']`:
*
* `$Name`: set to `"bob"`
*
* - POST `['test-name' => '']`:
*
* `$Name`: set to `""`, because it is not nullable
*
* - POST `['test-description' => 'abc']`:
*
* `$Description`: set to `abc`
*
* - POST `['test-description' => '']`:
*
* `$Description`: set to `null`, because it is nullable
*
* - POST `['test-chapter-number' => '456']`:
*
* `$ChapterNumber`: set to `456`
*
* - POST `['test-chapter-number' => '']`:
*
* `$ChapterNumber`: set to `null`, because an empty string sets nullable properties to `null`.
*/
public function PropertyFromHttp(string $property, \Enums\HttpVariableSource $set = POST, ?string $httpName = null): void{
try{
$rp = new \ReflectionProperty($this, $property);
}
catch(\ReflectionException){
return;
}
/** @var ?\ReflectionNamedType $propertyType */
$propertyType = $rp->getType();
if($propertyType !== null){
if($httpName === null){
$httpName = mb_strtolower(preg_replace('/([^^])([A-Z])/u', '\1-\2', $this::class . $property));
}
$vars = [];
switch($set){
case \Enums\HttpVariableSource::Get:
$vars = $_GET;
break;
case \Enums\HttpVariableSource::Post:
$vars = $_POST;
break;
case \Enums\HttpVariableSource::Cookie:
$vars = $_COOKIE;
break;
case \Enums\HttpVariableSource::Session:
$vars = $_SESSION;
break;
}
// If the variable was not passed to us, don't change the property.
if(!isset($vars[$httpName])){
return;
}
$type = $propertyType->getName();
$isPropertyNullable = $propertyType->allowsNull();
$isPropertyEnum = is_a($type, 'BackedEnum', true);
$postValue = null;
if($isPropertyEnum){
$postValue = $type::tryFrom(\HttpInput::Str($set, $httpName) ?? '');
}
else{
switch($type){
case 'int':
$postValue = \HttpInput::Int($set, $httpName);
break;
case 'bool':
$postValue = \HttpInput::Bool($set, $httpName);
break;
case 'float':
$postValue = \HttpInput::Dec($set, $httpName);
break;
case 'DateTimeImmutable':
case 'Safe\DateTimeImmutable':
$postValue = \HttpInput::Date($set, $httpName);
break;
case 'array':
$postValue = \HttpInput::Array($set, $httpName);
break;
case 'string':
$postValue = \HttpInput::Str($set, $httpName, true);
break;
}
}
if($type == 'string'){
if($isPropertyNullable){
if($postValue == ''){
$this->{$property} = null;
}
else{
$this->{$property} = $postValue;
}
}
elseif($postValue !== null){
$this->{$property} = $postValue;
}
}
elseif($isPropertyNullable || $postValue !== null){
$this->{$property} = $postValue;
}
}
}
}

View file

@ -17,10 +17,11 @@ class User{
public string $Uuid; public string $Uuid;
public ?string $PasswordHash = null; public ?string $PasswordHash = null;
protected ?bool $_IsRegistered = null; protected bool $_IsRegistered;
/** @var ?array<Payment> $_Payments */ /** @var array<Payment> $_Payments */
protected $_Payments = null; protected array $_Payments;
protected ?Benefits $_Benefits = null; protected Benefits $_Benefits;
// ******* // *******
// GETTERS // GETTERS
@ -30,7 +31,7 @@ class User{
* @return array<Payment> * @return array<Payment>
*/ */
protected function GetPayments(): array{ protected function GetPayments(): array{
if($this->_Payments === null){ if(!isset($this->_Payments)){
$this->_Payments = Db::Query(' $this->_Payments = Db::Query('
SELECT * SELECT *
from Payments from Payments
@ -43,7 +44,7 @@ class User{
} }
protected function GetBenefits(): Benefits{ protected function GetBenefits(): Benefits{
if($this->_Benefits === null){ if(!isset($this->_Benefits)){
$result = Db::Query(' $result = Db::Query('
SELECT * SELECT *
from Benefits from Benefits
@ -64,7 +65,7 @@ class User{
} }
protected function GetIsRegistered(): ?bool{ protected function GetIsRegistered(): ?bool{
if($this->_IsRegistered === null){ if(!isset($this->_IsRegistered)){
// A user is "registered" if they have a benefits entry in the table. // A user is "registered" if they have a benefits entry in the table.
// This function will fill it out for us. // This function will fill it out for us.
$this->GetBenefits(); $this->GetBenefits();

View file

@ -46,12 +46,7 @@ function CreateOpdsCollectionFeed(string $name, string $url, string $description
usort($collections, function($a, $b) use($collator){ usort($collections, function($a, $b) use($collator){
$result = $collator->compare($a['sortedname'], $b['sortedname']); $result = $collator->compare($a['sortedname'], $b['sortedname']);
if($result === false){ return $result === false ? 0 : $result;
return 0;
}
else{
return $result;
}
}); });
// Create the collections navigation document. // Create the collections navigation document.

View file

@ -208,7 +208,7 @@ try{
) )
){ ){
// This payment is eligible for the Patrons Circle! // This payment is eligible for the Patrons Circle!
if($payment->User !== null){ if($payment->UserId !== null && $payment->User !== null){
// Are we already a patron? // Are we already a patron?
if(!Db::QueryBool(' if(!Db::QueryBool('
SELECT exists( SELECT exists(

View file

@ -34,10 +34,10 @@ $isEditForm = $isEditForm ?? false;
<label class="year"> <label class="year">
<span>Year of death</span> <span>Year of death</span>
<span>If circa or unknown, enter the latest possible year.</span> <span>If circa or unknown, enter the latest possible year.</span>
<? /* Not using <input type="number"> for now, see https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/ */ ?> <? /* Not using `<input type="number">` for now, see <https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/>. */ ?>
<input <input
type="text" type="text"
name="artist-year-of-death" name="artist-death-year"
inputmode="numeric" inputmode="numeric"
pattern="[0-9]{1,4}" pattern="[0-9]{1,4}"
value="<?= Formatter::EscapeHtml((string)$artwork->Artist->DeathYear) ?>" value="<?= Formatter::EscapeHtml((string)$artwork->Artist->DeathYear) ?>"
@ -56,7 +56,7 @@ $isEditForm = $isEditForm ?? false;
Year of completion Year of completion
<input <input
type="text" type="text"
name="artwork-year" name="artwork-completed-year"
inputmode="numeric" inputmode="numeric"
pattern="[0-9]{1,4}" pattern="[0-9]{1,4}"
value="<?= Formatter::EscapeHtml((string)$artwork->CompletedYear) ?>" value="<?= Formatter::EscapeHtml((string)$artwork->CompletedYear) ?>"
@ -65,7 +65,7 @@ $isEditForm = $isEditForm ?? false;
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="artwork-year-is-circa" name="artwork-completed-year-is-circa"
<? if($artwork->CompletedYearIsCirca){ ?>checked="checked"<? } ?> <? if($artwork->CompletedYearIsCirca){ ?>checked="checked"<? } ?>
/> Year is circa /> Year is circa
</label> </label>
@ -172,7 +172,7 @@ $isEditForm = $isEditForm ?? false;
</label> </label>
</fieldset> </fieldset>
<? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null) || $artwork->CanEbookUrlBeChangedBy($GLOBALS['User'] ?? null)){ ?> <? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null) || $artwork->CanEbookUrlBeChangedBy($GLOBALS['User'] ?? null)){ ?>
<fieldset> <fieldset>
<legend>Editor options</legend> <legend>Editor options</legend>
<? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null)){ ?> <? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null)){ ?>
<label> <label>
@ -193,7 +193,7 @@ $isEditForm = $isEditForm ?? false;
<input type="url" name="artwork-ebook-url" placeholder="https://standardebooks.org/ebooks/" pattern="^https:\/\/standardebooks\.org\/ebooks/[^\/]+(\/[^\/]+)+$" value="<?= Formatter::EscapeHtml($artwork->EbookUrl) ?>"/> <input type="url" name="artwork-ebook-url" placeholder="https://standardebooks.org/ebooks/" pattern="^https:\/\/standardebooks\.org\/ebooks/[^\/]+(\/[^\/]+)+$" value="<?= Formatter::EscapeHtml($artwork->EbookUrl) ?>"/>
</label> </label>
<? } ?> <? } ?>
</fieldset> </fieldset>
<? } ?> <? } ?>
<div class="footer"> <div class="footer">
<button><? if($isEditForm){ ?>Save changes<? }else{ ?>Submit<? } ?></button> <button><? if($isEditForm){ ?>Save changes<? }else{ ?>Submit<? } ?></button>

View file

@ -39,7 +39,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
<entry> <entry>
<title><?= Formatter::EscapeXml($entry->Title) ?></title> <title><?= Formatter::EscapeXml($entry->Title) ?></title>
<link href="<?= SITE_URL . Formatter::EscapeXml($entry->Url) ?>" rel="<?= Formatter::EscapeXml($entry->Rel) ?>" type="application/atom+xml;profile=opds-catalog;kind=<?= $entry->Type ?>; charset=utf-8"/> <link href="<?= SITE_URL . Formatter::EscapeXml($entry->Url) ?>" rel="<?= Formatter::EscapeXml($entry->Rel) ?>" type="application/atom+xml;profile=opds-catalog;kind=<?= $entry->Type ?>; charset=utf-8"/>
<updated><? if($entry->Updated !== null){ ?><?= $entry->Updated->format(Enums\DateTimeFormat::Iso->value) ?><? } ?></updated> <updated><?= $entry->Updated->format(Enums\DateTimeFormat::Iso->value) ?></updated>
<id><?= Formatter::EscapeXml($entry->Id) ?></id> <id><?= Formatter::EscapeXml($entry->Id) ?></id>
<content type="text"><?= Formatter::EscapeXml($entry->Description) ?></content> <content type="text"><?= Formatter::EscapeXml($entry->Description) ?></content>
</entry> </entry>

View file

@ -19,7 +19,9 @@ try{
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }
$artwork = Artwork::FromHttpPost(); $artwork = new Artwork();
$artwork->FillFromHttpPost();
$artwork->SubmitterUserId = $GLOBALS['User']->UserId ?? null; $artwork->SubmitterUserId = $GLOBALS['User']->UserId ?? null;
// Only approved reviewers can set the status to anything but unverified when uploading. // Only approved reviewers can set the status to anything but unverified when uploading.
@ -28,7 +30,7 @@ try{
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }
// If the artwork is approved, set the reviewer // If the artwork is approved, set the reviewer.
if($artwork->Status !== Enums\ArtworkStatusType::Unverified){ if($artwork->Status !== Enums\ArtworkStatusType::Unverified){
$artwork->ReviewerUserId = $GLOBALS['User']->UserId; $artwork->ReviewerUserId = $GLOBALS['User']->UserId;
} }
@ -56,7 +58,7 @@ try{
$artwork->ArtworkId = $originalArtwork->ArtworkId; $artwork->ArtworkId = $originalArtwork->ArtworkId;
$artwork->Created = $originalArtwork->Created; $artwork->Created = $originalArtwork->Created;
$artwork->SubmitterUserId = $originalArtwork->SubmitterUserId; $artwork->SubmitterUserId = $originalArtwork->SubmitterUserId;
$artwork->Status = $originalArtwork->Status; // Overwrite any value got from POST because we need permission to change the status $artwork->Status = $originalArtwork->Status; // Overwrite any value got from POST because we need permission to change the status.
$newStatus = Enums\ArtworkStatusType::tryFrom(HttpInput::Str(POST, 'artwork-status') ?? ''); $newStatus = Enums\ArtworkStatusType::tryFrom(HttpInput::Str(POST, 'artwork-status') ?? '');
if($newStatus !== null){ if($newStatus !== null){
@ -96,10 +98,13 @@ try{
} }
$artwork->ReviewerUserId = $GLOBALS['User']->UserId; $artwork->ReviewerUserId = $GLOBALS['User']->UserId;
}
$artwork->Status = $newStatus; $artwork->Status = $newStatus;
} }
else{
unset($artwork->Status);
}
}
if(isset($_POST['artwork-ebook-url'])){ if(isset($_POST['artwork-ebook-url'])){
$newEbookUrl = HttpInput::Str(POST, 'artwork-ebook-url'); $newEbookUrl = HttpInput::Str(POST, 'artwork-ebook-url');

View file

@ -162,13 +162,7 @@ catch(Exceptions\EbookNotFoundException){
<?= Template::DonationAlert() ?> <?= Template::DonationAlert() ?>
<? if($ebook->LongDescription === null){ ?>
<p>
<i>Theres no description for this ebook yet.</i>
</p>
<? }else{ ?>
<?= $ebook->LongDescription ?> <?= $ebook->LongDescription ?>
<? } ?>
</section> </section>
<? if($ebook->HasDownloads){ ?> <? if($ebook->HasDownloads){ ?>

View file

@ -30,7 +30,6 @@ try{
exit(); exit();
} }
$subscription->User = new User(); $subscription->User = new User();
$subscription->User->Email = HttpInput::Str(POST, 'email'); $subscription->User->Email = HttpInput::Str(POST, 'email');
$subscription->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'issubscribedtonewsletter') ?? false; $subscription->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'issubscribedtonewsletter') ?? false;

View file

@ -17,7 +17,9 @@ try{
/** @var PollVote $vote */ /** @var PollVote $vote */
$vote = $_SESSION['vote']; $vote = $_SESSION['vote'];
} }
else{
if(!isset($vote->UserId)){
$vote->UserId = $GLOBALS['User']->UserId;
$vote->User = $GLOBALS['User']; $vote->User = $GLOBALS['User'];
} }
@ -59,14 +61,14 @@ catch(Exceptions\PollVoteExistsException $ex){
<h1>Vote in the <?= Formatter::EscapeHtml($poll->Name) ?> Poll</h1> <h1>Vote in the <?= Formatter::EscapeHtml($poll->Name) ?> Poll</h1>
<?= Template::Error(['exception' => $exception]) ?> <?= Template::Error(['exception' => $exception]) ?>
<form method="post" action="<?= Formatter::EscapeHtml($poll->Url) ?>/votes"> <form method="post" action="<?= Formatter::EscapeHtml($poll->Url) ?>/votes">
<input type="hidden" name="email" value="<? if($vote->User !== null){ ?><?= Formatter::EscapeHtml($vote->User->Email) ?><? } ?>" maxlength="80" required="required" /> <input type="hidden" name="email" value="<?= Formatter::EscapeHtml($vote->User->Email) ?>" maxlength="80" required="required" />
<fieldset> <fieldset>
<p>Select one of these options.</p> <p>Select one of these options.</p>
<ul> <ul>
<? foreach($poll->PollItems as $pollItem){ ?> <? foreach($poll->PollItems as $pollItem){ ?>
<li> <li>
<label class="checkbox"> <label class="checkbox">
<input type="radio" value="<?= $pollItem->PollItemId ?>" name="pollitemid" required="required"<? if($vote->PollItemId == $pollItem->PollItemId){ ?> checked="checked"<? } ?>/> <input type="radio" value="<?= $pollItem->PollItemId ?>" name="poll-vote-poll-item-id" required="required"<? if(isset($vote->PollItemId) && $vote->PollItemId == $pollItem->PollItemId){ ?> checked="checked"<? } ?>/>
<span> <span>
<b><?= $pollItem->Name ?></b> <b><?= $pollItem->Name ?></b>
<? if($pollItem->Description !== null){ ?> <? if($pollItem->Description !== null){ ?>

View file

@ -10,7 +10,7 @@ try{
$vote = new PollVote(); $vote = new PollVote();
$vote->PollItemId = HttpInput::Int(POST, 'pollitemid'); $vote->FillFromHttpPost();
$vote->Create(HttpInput::Str(POST, 'email')); $vote->Create(HttpInput::Str(POST, 'email'));