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,
`Created` timestamp NOT NULL DEFAULT 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,
`ReviewerUserId` int(10) unsigned NULL,
`MuseumUrl` varchar(255) NULL,

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS `EbookSources` (
`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,
`SortOrder` tinyint(3) unsigned NOT NULL,
KEY `index1` (`EbookId`)

View file

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

View file

@ -7,6 +7,7 @@ use function Safe\getimagesize;
use function Safe\parse_url;
use function Safe\preg_match;
use function Safe\preg_replace;
use function Safe\unlink;
/**
* @property string $UrlName
@ -24,19 +25,20 @@ use function Safe\preg_replace;
* @property string $Dimensions
* @property Ebook $Ebook
* @property Museum $Museum
* @property User $Submitter
* @property User $Reviewer
* @property ?User $Submitter
* @property ?User $Reviewer
*/
class Artwork{
use Traits\Accessor;
use Traits\PropertyFromHttp;
public ?string $Name = null;
public ?int $ArtworkId = null;
public ?int $ArtistId = null;
public int $ArtworkId;
public string $Name = '';
public int $ArtistId;
public ?int $CompletedYear = null;
public bool $CompletedYearIsCirca = false;
public ?DateTimeImmutable $Created = null;
public ?DateTimeImmutable $Updated = null;
public DateTimeImmutable $Created;
public DateTimeImmutable $Updated;
public ?string $EbookUrl = null;
public ?int $SubmitterUserId = null;
public ?int $ReviewerUserId = null;
@ -45,26 +47,27 @@ class Artwork{
public ?string $PublicationYearPageUrl = null;
public ?string $CopyrightPageUrl = null;
public ?string $ArtworkPageUrl = null;
public ?bool $IsPublishedInUs = null;
public bool $IsPublishedInUs = false;
public ?string $Exception = null;
public ?string $Notes = null;
public ?Enums\ImageMimeType $MimeType = null;
public ?Enums\ArtworkStatusType $Status = null;
public Enums\ImageMimeType $MimeType;
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
@ -74,45 +77,54 @@ class Artwork{
* @param string|null|array<ArtworkTag> $tags
*/
protected function SetTags(null|string|array $tags): void{
if($tags === null || is_array($tags)){
if(is_array($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_values(array_filter($tags));
$tags = array_unique($tags);
$this->_Tags = array_map(function ($str){
$this->_Tags = array_map(function ($str): ArtworkTag{
$tag = new ArtworkTag();
$tag->Name = $str;
return $tag;
}, $tags);
}
}
}
// *******
// GETTERS
// *******
protected function GetUrlName(): string{
if($this->Name === null || $this->Name == ''){
return '';
if(!isset($this->_UrlName)){
if(!isset($this->Name) || $this->Name == ''){
$this->_UrlName = '';
}
if($this->_UrlName === null){
else{
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
}
}
return $this->_UrlName;
}
protected function GetSubmitter(): ?User{
if($this->_Submitter === null){
if(!isset($this->_Submitter)){
try{
$this->_Submitter = User::Get($this->SubmitterUserId);
}
catch(Exceptions\UserNotFoundException){
// Return null
$this->Submitter = null;
}
}
@ -120,12 +132,12 @@ class Artwork{
}
protected function GetReviewer(): ?User{
if($this->_Reviewer === null){
if(!isset($this->_Reviewer)){
try{
$this->_Reviewer = User::Get($this->ReviewerUserId);
}
catch(Exceptions\UserNotFoundException){
// Return null
$this->_Reviewer = null;
}
}
@ -133,7 +145,7 @@ class Artwork{
}
protected function GetUrl(): string{
if($this->_Url === null){
if(!isset($this->_Url)){
$this->_Url = '/artworks/' . $this->Artist->UrlName . '/' . $this->UrlName;
}
@ -141,7 +153,7 @@ class Artwork{
}
protected function GetEditUrl(): string{
if($this->_EditUrl === null){
if(!isset($this->_EditUrl)){
$this->_EditUrl = $this->Url . '/edit';
}
@ -152,7 +164,7 @@ class Artwork{
* @return array<ArtworkTag>
*/
protected function GetTags(): array{
if($this->_Tags === null){
if(!isset($this->_Tags)){
$this->_Tags = Db::Query('
SELECT t.*
from Tags t
@ -168,34 +180,28 @@ class Artwork{
* @throws Exceptions\InvalidUrlException
*/
public function GetMuseum(): ?Museum{
if($this->_Museum === null){
if(!isset($this->_Museum)){
try{
$this->_Museum = Museum::GetByUrl($this->MuseumUrl);
}
catch(Exceptions\MuseumNotFoundException){
// Pass
// Pass.
}
}
return $this->_Museum;
}
public function ImplodeTags(): string{
$tags = $this->Tags ?? [];
$tags = array_column($tags, 'Name');
return trim(implode(', ', $tags));
}
/**
* @throws Exceptions\InvalidArtworkException
*/
protected function GetImageUrl(): string{
if($this->_ImageUrl === null){
if($this->ArtworkId === null || $this->MimeType === null){
if(!isset($this->_ImageUrl)){
if(!isset($this->ArtworkId) || !isset($this->MimeType)){
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;
@ -205,12 +211,12 @@ class Artwork{
* @throws Exceptions\ArtworkNotFoundException
*/
protected function GetThumbUrl(): string{
if($this->_ThumbUrl === null){
if($this->ArtworkId === null){
if(!isset($this->_ThumbUrl)){
if(!isset($this->ArtworkId)){
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;
@ -220,12 +226,12 @@ class Artwork{
* @throws Exceptions\ArtworkNotFoundException
*/
protected function GetThumb2xUrl(): string{
if($this->_Thumb2xUrl === null){
if($this->ArtworkId === null){
if(!isset($this->_Thumb2xUrl)){
if(!isset($this->ArtworkId)){
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;
@ -244,6 +250,7 @@ class Artwork{
}
protected function GetDimensions(): string{
if(!isset($this->Dimensions)){
$this->_Dimensions = '';
try{
list($imageWidth, $imageHeight) = getimagesize($this->ImageFsPath);
@ -252,14 +259,15 @@ class Artwork{
}
}
catch(Exception){
// Image doesn't exist, return blank string
// Image doesn't exist, return a blank string.
}
}
return $this->_Dimensions;
}
protected function GetEbook(): ?Ebook{
if($this->_Ebook === null){
if(!isset($this->_Ebook)){
if($this->EbookUrl === null){
return null;
}
@ -277,9 +285,11 @@ class Artwork{
return $this->_Ebook;
}
// *******
// METHODS
// *******
public function CanBeEditedBy(?User $user): bool{
if($user === null){
return false;
@ -336,16 +346,17 @@ class Artwork{
$thisYear = intval(NOW->format('Y'));
$error = new Exceptions\InvalidArtworkException();
if($this->Artist === null){
if(!isset($this->Artist)){
$error->Add(new Exceptions\InvalidArtistException());
}
else{
try{
$this->Artist->Validate();
}
catch(Exceptions\ValidationException $ex){
$error->Add($ex);
}
}
if($this->Exception !== null && trim($this->Exception) == ''){
$this->Exception = null;
@ -355,13 +366,18 @@ class Artwork{
$this->Notes = null;
}
if($this->Name === null || $this->Name == ''){
if(isset($this->Name)){
if($this->Name == ''){
$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'));
}
}
else{
$error->Add(new Exceptions\ArtworkNameRequiredException());
}
if($this->CompletedYear !== null && ($this->CompletedYear <= 0 || $this->CompletedYear > $thisYear)){
$error->Add(new Exceptions\InvalidCompletedYearException());
@ -375,10 +391,11 @@ class Artwork{
$error->Add(new Exceptions\InvalidPublicationYearException());
}
if($this->Status === null){
if(!isset($this->Status)){
$error->Add(new Exceptions\InvalidArtworkException('Invalid status.'));
}
if(isset($this->Tags)){
if(count($this->Tags) == 0){
$error->Add(new Exceptions\TagsRequiredException());
}
@ -395,6 +412,10 @@ class Artwork{
$error->Add($ex);
}
}
}
else{
$error->Add(new Exceptions\TagsRequiredException());
}
if($this->MuseumUrl !== null){
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{
$existingArtwork = Artwork::GetByUrl($this->Artist->UrlName, $this->UrlName);
if($existingArtwork->ArtworkId != $this->ArtworkId){
// Duplicate found, alert the user
// Duplicate found, alert the user.
$error->Add(new Exceptions\ArtworkAlreadyExistsException());
}
}
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.'));
}
if($imagePath !== null && $this->MimeType !== null){
else{
if(!is_writable(WEB_ROOT . COVER_ART_UPLOAD_PATH)){
$error->Add(new Exceptions\InvalidImageUploadException('Upload path not writable.'));
}
// Check for minimum dimensions
// Check for minimum dimensions.
list($imageWidth, $imageHeight) = getimagesize($imagePath);
if(!$imageWidth || !$imageHeight || $imageWidth < ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < ARTWORK_IMAGE_MINIMUM_HEIGHT){
$error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException());
}
}
}
if($imagePath !== null && $this->MimeType === null && !$error->Has('Exceptions\InvalidImageUploadException')){
// Only notify of wrong mimetype if there are no other problem with the uploaded image
if(!isset($this->MimeType)){
$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\InvalidPageScanUrlException
@ -661,11 +690,12 @@ class Artwork{
* @throws Exceptions\InvalidImageUploadException
*/
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->Created = NOW;
$this->Updated = NOW;
$tags = [];
foreach($this->Tags as $artworkTag){
@ -677,7 +707,7 @@ class Artwork{
Db::Query('
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,
EbookUrl, MimeType, Exception, Notes)
values (?,
@ -698,9 +728,10 @@ class Artwork{
?,
?,
?,
?,
?)
', [$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]
);
@ -726,16 +757,16 @@ class Artwork{
* @throws Exceptions\InvalidImageUploadException
*/
public function Save(?string $imagePath = null): void{
$this->_UrlName = null;
unset($this->_UrlName);
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.
$this->Updated = NOW;
$this->_ImageUrl = null;
$this->_ThumbUrl = null;
$this->_Thumb2xUrl = null;
unset($this->_ImageUrl);
unset($this->_ThumbUrl);
unset($this->_Thumb2xUrl);
}
$this->Validate($imagePath, false);
@ -828,7 +859,29 @@ class Artwork{
from Artworks
where 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
@ -871,14 +924,10 @@ class Artwork{
public static function FromHttpPost(): Artwork{
$artwork = new Artwork();
$artwork->Artist = new Artist();
$artwork->Artist->Name = HttpInput::Str(POST, 'artist-name');
$artwork->Artist->DeathYear = HttpInput::Int(POST, 'artist-year-of-death');
$artwork->Name = HttpInput::Str(POST, 'artwork-name');
$artwork->Name = HttpInput::Str(POST, 'artwork-name') ?? '';
$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->Status = Enums\ArtworkStatusType::tryFrom(HttpInput::Str(POST, 'artwork-status') ?? '') ?? Enums\ArtworkStatusType::Unverified;
$artwork->EbookUrl = HttpInput::Str(POST, 'artwork-ebook-url');
@ -893,4 +942,28 @@ class 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;
}
// *******
// GETTERS
// *******
protected function GetUrl(): string{
if($this->_Url === null){
if(!isset($this->_Url)){
$this->_Url = '/artworks?query=' . Formatter::MakeUrlSafe($this->Name);
}
return $this->_Url;
}
// *******
// METHODS
// *******
@ -29,6 +31,7 @@ class ArtworkTag extends Tag{
public function Validate(): void{
$error = new Exceptions\InvalidArtworkTagException($this->Name);
if(isset($this->Name)){
$this->Name = mb_strtolower(trim($this->Name));
// Collapse spaces into one
$this->Name = preg_replace('/[\s]+/ius', ' ', $this->Name);
@ -45,6 +48,12 @@ class ArtworkTag extends Tag{
$error->Add(new Exceptions\InvalidArtworkTagNameException());
}
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
}
else{
$error->Add(new Exceptions\ArtworkTagNameRequiredException());
}
if($this->Type != Enums\TagType::Artwork){
$error->Add(new Exceptions\InvalidArtworkTagTypeException($this->Type));
}
@ -61,10 +70,11 @@ class ArtworkTag extends Tag{
$this->Validate();
Db::Query('
INSERT into Tags (Name, Type)
INSERT into Tags (Name, UrlName, Type)
values (?,
?,
?)
', [$this->Name, $this->Type]);
', [$this->Name, $this->UrlName, $this->Type]);
$this->TagId = Db::GetLastInsertedId();
}

View file

@ -19,18 +19,19 @@ class AtomFeed extends Feed{
$this->Stylesheet = SITE_URL . '/feeds/atom/style';
}
// *******
// METHODS
// *******
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]);
$this->XmlString = $this->CleanXmlString($feed);
$this->_XmlString = $this->CleanXmlString($feed);
}
return $this->XmlString;
return $this->_XmlString;
}
public function SaveIfChanged(): bool{
@ -58,7 +59,7 @@ class AtomFeed extends Feed{
$obj->Id = SITE_URL . $entry->Url;
}
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;
}
$currentEntries[] = $obj;

View file

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

View file

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

View file

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

View file

@ -4,25 +4,20 @@ class EbookTag extends Tag{
$this->Type = Enums\TagType::Ebook;
}
// *******
// GETTERS
// *******
protected function GetUrlName(): string{
if($this->_UrlName === null){
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
}
return $this->_UrlName;
}
protected function GetUrl(): string{
if($this->_Url === null){
if(!isset($this->_Url)){
$this->_Url = '/subjects/' . $this->UrlName;
}
return $this->_Url;
}
// *******
// METHODS
// *******
@ -43,6 +38,8 @@ class EbookTag extends Tag{
if(strlen($this->Name) > EBOOKS_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook tag: '. $this->Name));
}
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
}
else{
$error->Add(new Exceptions\EbookTagNameRequiredException());
@ -72,6 +69,11 @@ class EbookTag extends Tag{
$this->TagId = Db::GetLastInsertedId();
}
// ***********
// ORM METHODS
// ***********
/**
* @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 string $Path;
public ?string $Stylesheet = null;
protected ?string $XmlString = null;
public ?DateTimeImmutable $Updated = null;
protected string $_XmlString;
public DateTimeImmutable $Updated;
/**
* @param string $title
@ -31,6 +31,13 @@ abstract class Feed{
}
// *******
// GETTERS
// *******
abstract protected function GetXmlString(): string;
// *******
// METHODS
// *******
@ -52,11 +59,6 @@ abstract class Feed{
return $output;
}
protected function GetXmlString(): string{
// Virtual function, meant to be implemented by subclass
return '';
}
public function Save(): void{
$feed = $this->GetXmlString();

View file

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

View file

@ -62,7 +62,7 @@ class Image{
unlink($tempFilename);
}
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\filesize;
use function Safe\glob;
use function Safe\ksort;
use function Safe\preg_replace;
use function Safe\preg_split;
use function Safe\sprintf;
use function Safe\usort;
class Library{
/**
* @param array<string> $tags
*
* @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{
@ -102,11 +100,11 @@ class Library{
', [$urlPath], Ebook::class);
}
else{
// Multiple authors, e.g., karl-marx_friedrich-engels
// Multiple authors, e.g., `karl-marx_friedrich-engels`.
$authors = explode('_', $urlPath);
$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('
SELECT e.*
@ -123,6 +121,7 @@ class Library{
/**
* @return array<Collection>
*
* @throws Exceptions\AppException
*/
public static function GetEbookCollections(): array{
@ -135,7 +134,10 @@ class Library{
if($collator === null){
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;
}
@ -344,6 +346,7 @@ class Library{
/**
* @return array<Artwork>
*
* @throws Exceptions\ArtistNotFoundException
*/
public static function GetArtworksByArtist(?string $artistUrlName, ?string $status, ?int $submitterUserId): array{
@ -451,7 +454,8 @@ class Library{
/**
* @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{
// 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
*/
public static function RebuildBulkDownloadsCache(): array{
@ -489,6 +494,7 @@ class Library{
if($collator === null){
throw new Exceptions\AppException('Couldn\'t create collator object when rebuilding bulk download cache.');
}
/** @var array<string, array<string, stdClass>> $months */
$months = [];
$subjects = [];
$collections = [];
@ -509,6 +515,8 @@ class Library{
catch(\Exception){
throw new Exceptions\AppException('Couldn\'t parse date on bulk download object.');
}
/** @var string $year Required to satisfy PHPStan */
$year = $date->format('Y');
$month = $date->format('F');
@ -533,7 +541,10 @@ class Library{
foreach(glob(WEB_ROOT . '/bulk-downloads/collections/*/', GLOB_NOSORT) as $dir){
$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
@ -541,7 +552,10 @@ class Library{
foreach(glob(WEB_ROOT . '/bulk-downloads/authors/*/', GLOB_NOSORT) as $dir){
$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
@ -549,7 +563,8 @@ class Library{
}
/**
* @return array<string, array<int|string, array<int|string, mixed>>>
* @return array<stdClass>
*
* @throws Exceptions\AppException
*/
public static function RebuildFeedsCache(?string $returnType = null, ?string $returnClass = null): ?array{
@ -584,7 +599,10 @@ class Library{
$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){
$retval = $feeds;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,12 +6,12 @@ class OpdsNavigationEntry{
public string $Url;
public string $Rel;
public string $Type;
public ?DateTimeImmutable $Updated = null;
public DateTimeImmutable $Updated;
public string $Description;
public string $Title;
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->Url = $url;
$this->Rel = $rel;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,14 +8,15 @@ use Safe\DateTimeImmutable;
*/
class PollVote{
use Traits\Accessor;
use Traits\PropertyFromHttp;
public ?int $UserId = null;
public int $UserId;
public DateTimeImmutable $Created;
public ?int $PollItemId = null;
public int $PollItemId;
protected ?User $_User = null;
protected ?PollItem $_PollItem = null;
protected ?string $_Url = null;
protected User $_User;
protected PollItem $_PollItem;
protected string $_Url;
// *******
@ -23,7 +24,7 @@ class PollVote{
// *******
protected function GetUrl(): string{
if($this->_Url === null){
if(!isset($this->_Url)){
$this->_Url = $this->PollItem->Poll->Url . '/votes/' . $this->UserId;
}
@ -41,26 +42,30 @@ class PollVote{
protected function Validate(): void{
$error = new Exceptions\InvalidPollVoteException();
if($this->User === null){
if(!isset($this->UserId)){
$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());
}
else{
if($this->PollItem === null){
$error->Add(new Exceptions\PollNotFoundException());
}
else{
if($this->PollItem->Poll === null){
$error->Add(new Exceptions\PollNotFoundException());
}
else{
try{
if(!$this->PollItem->Poll->IsActive()){
$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;
}
catch(Exceptions\UserNotFoundException){
// Can't validate patron email - do nothing for now,
// 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.
// Can't validate patron email - do nothing for now, 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.
$this->User = new User();
$this->User->Email = $email;
}
@ -135,4 +138,8 @@ class PollVote{
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{
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]);
$this->XmlString = $this->CleanXmlString($feed);
$this->_XmlString = $this->CleanXmlString($feed);
}
return $this->XmlString;
return $this->_XmlString;
}
public function SaveIfChanged(): bool{

View file

@ -13,8 +13,8 @@ class Session{
public DateTimeImmutable $Created;
public string $SessionId;
protected ?User $_User = null;
public ?string $_Url = null;
protected User $_User;
public string $_Url;
// *******
@ -22,7 +22,7 @@ class Session{
// *******
protected function GetUrl(): string{
if($this->_Url === null){
if(!isset($this->_Url)){
$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
}
// ***********
// ORM METHODS
// ***********
/**
* @throws Exceptions\SessionNotFoundException
*/

View file

@ -1,14 +1,14 @@
<?
/**
* @property string $Url
* @property string $UrlName
*/
class Tag{
use Traits\Accessor;
public int $TagId;
public string $Name;
public string $UrlName;
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();
*
* isset($t->User); // true
* ```
*
* @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 $PasswordHash = null;
protected ?bool $_IsRegistered = null;
/** @var ?array<Payment> $_Payments */
protected $_Payments = null;
protected ?Benefits $_Benefits = null;
protected bool $_IsRegistered;
/** @var array<Payment> $_Payments */
protected array $_Payments;
protected Benefits $_Benefits;
// *******
// GETTERS
@ -30,7 +31,7 @@ class User{
* @return array<Payment>
*/
protected function GetPayments(): array{
if($this->_Payments === null){
if(!isset($this->_Payments)){
$this->_Payments = Db::Query('
SELECT *
from Payments
@ -43,7 +44,7 @@ class User{
}
protected function GetBenefits(): Benefits{
if($this->_Benefits === null){
if(!isset($this->_Benefits)){
$result = Db::Query('
SELECT *
from Benefits
@ -64,7 +65,7 @@ class User{
}
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.
// This function will fill it out for us.
$this->GetBenefits();

View file

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

View file

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

View file

@ -34,10 +34,10 @@ $isEditForm = $isEditForm ?? false;
<label class="year">
<span>Year of death</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
type="text"
name="artist-year-of-death"
name="artist-death-year"
inputmode="numeric"
pattern="[0-9]{1,4}"
value="<?= Formatter::EscapeHtml((string)$artwork->Artist->DeathYear) ?>"
@ -56,7 +56,7 @@ $isEditForm = $isEditForm ?? false;
Year of completion
<input
type="text"
name="artwork-year"
name="artwork-completed-year"
inputmode="numeric"
pattern="[0-9]{1,4}"
value="<?= Formatter::EscapeHtml((string)$artwork->CompletedYear) ?>"
@ -65,7 +65,7 @@ $isEditForm = $isEditForm ?? false;
<label>
<input
type="checkbox"
name="artwork-year-is-circa"
name="artwork-completed-year-is-circa"
<? if($artwork->CompletedYearIsCirca){ ?>checked="checked"<? } ?>
/> Year is circa
</label>
@ -172,7 +172,7 @@ $isEditForm = $isEditForm ?? false;
</label>
</fieldset>
<? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null) || $artwork->CanEbookUrlBeChangedBy($GLOBALS['User'] ?? null)){ ?>
<fieldset>
<fieldset>
<legend>Editor options</legend>
<? if($artwork->CanStatusBeChangedBy($GLOBALS['User'] ?? null)){ ?>
<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) ?>"/>
</label>
<? } ?>
</fieldset>
</fieldset>
<? } ?>
<div class="footer">
<button><? if($isEditForm){ ?>Save changes<? }else{ ?>Submit<? } ?></button>

View file

@ -39,7 +39,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
<entry>
<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"/>
<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>
<content type="text"><?= Formatter::EscapeXml($entry->Description) ?></content>
</entry>

View file

@ -19,7 +19,9 @@ try{
throw new Exceptions\InvalidPermissionsException();
}
$artwork = Artwork::FromHttpPost();
$artwork = new Artwork();
$artwork->FillFromHttpPost();
$artwork->SubmitterUserId = $GLOBALS['User']->UserId ?? null;
// Only approved reviewers can set the status to anything but unverified when uploading.
@ -28,7 +30,7 @@ try{
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){
$artwork->ReviewerUserId = $GLOBALS['User']->UserId;
}
@ -56,7 +58,7 @@ try{
$artwork->ArtworkId = $originalArtwork->ArtworkId;
$artwork->Created = $originalArtwork->Created;
$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') ?? '');
if($newStatus !== null){
@ -96,10 +98,13 @@ try{
}
$artwork->ReviewerUserId = $GLOBALS['User']->UserId;
}
$artwork->Status = $newStatus;
}
else{
unset($artwork->Status);
}
}
if(isset($_POST['artwork-ebook-url'])){
$newEbookUrl = HttpInput::Str(POST, 'artwork-ebook-url');

View file

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

View file

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

View file

@ -17,7 +17,9 @@ try{
/** @var PollVote $vote */
$vote = $_SESSION['vote'];
}
else{
if(!isset($vote->UserId)){
$vote->UserId = $GLOBALS['User']->UserId;
$vote->User = $GLOBALS['User'];
}
@ -59,14 +61,14 @@ catch(Exceptions\PollVoteExistsException $ex){
<h1>Vote in the <?= Formatter::EscapeHtml($poll->Name) ?> Poll</h1>
<?= Template::Error(['exception' => $exception]) ?>
<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>
<p>Select one of these options.</p>
<ul>
<? foreach($poll->PollItems as $pollItem){ ?>
<li>
<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>
<b><?= $pollItem->Name ?></b>
<? if($pollItem->Description !== null){ ?>

View file

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