web/lib/Artwork.php
2024-01-08 20:21:42 -06:00

603 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?
use Safe\DateTime;
use function Safe\copy;
use function Safe\date;
use function Safe\exec;
use function Safe\getimagesize;
use function Safe\ini_get;
use function Safe\preg_replace;
/**
* @property string $UrlName
* @property string $Url
* @property array<ArtworkTag> $Tags
* @property Artist $Artist
* @property string $ImageUrl
* @property string $ThumbUrl
* @property string $Thumb2xUrl
* @property string $Dimensions
* @property Ebook $Ebook
* @property Museum $Museum
* @property User $Submitter
* @property User $Reviewer
* @property ?ImageMimeType $MimeType
* @property array<ArtworkTag> $_Tags
*/
class Artwork extends PropertiesBase{
public ?string $Name = null;
public ?int $ArtworkId = null;
public ?int $ArtistId = null;
public ?int $CompletedYear = null;
public ?bool $CompletedYearIsCirca = null;
public ?DateTime $Created = null;
public ?DateTime $Updated = null;
public ?string $Status = null;
public ?string $EbookWwwFilesystemPath = null;
public ?int $SubmitterUserId = null;
public ?int $ReviewerUserId = null;
public ?string $MuseumUrl = null;
public ?int $PublicationYear = null;
public ?string $PublicationYearPageUrl = null;
public ?string $CopyrightPageUrl = null;
public ?string $ArtworkPageUrl = null;
public ?bool $IsPublishedInUs = null;
public ?string $Exception = null;
protected ?string $_UrlName = null;
protected ?string $_Url = null;
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;
protected ?ImageMimeType $_MimeType = null;
// *******
// GETTERS
// *******
protected function GetUrlName(): string{
if($this->Name === null || $this->Name == ''){
return '';
}
if($this->_UrlName === null){
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
}
return $this->_UrlName;
}
protected function GetSubmitter(): ?User{
if($this->_Submitter === null){
try{
$this->_Submitter = User::Get($this->SubmitterUserId);
}
catch(Exceptions\InvalidUserException){
// Return null
}
}
return $this->_Submitter;
}
protected function GetReviewer(): ?User{
if($this->_Reviewer === null){
try{
$this->_Reviewer = User::Get($this->ReviewerUserId);
}
catch(Exceptions\InvalidUserException){
// Return null
}
}
return $this->_Reviewer;
}
protected function GetUrl(): string{
if($this->_Url === null){
$this->_Url = '/artworks/' . $this->Artist->UrlName . '/' . $this->UrlName;
}
return $this->_Url;
}
/**
* @return array<ArtworkTag>
*/
protected function GetTags(): array{
if($this->_Tags === null){
$this->_Tags = Db::Query('
SELECT t.*
from Tags t
inner join ArtworkTags at using (TagId)
where ArtworkId = ?
', [$this->ArtworkId], 'ArtworkTag');
}
return $this->_Tags;
}
public function GetMuseum(): ?Museum{
if($this->_Museum === null){
try{
$this->_Museum = Museum::GetByUrl($this->MuseumUrl);
}
catch(Exceptions\MuseumNotFoundException){
// 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){
throw new Exceptions\InvalidArtworkException();
}
$this->_ImageUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . $this->MimeType->GetFileExtension();
}
return $this->_ImageUrl;
}
/**
* @throws \Exceptions\ArtworkNotFoundException
*/
protected function GetThumbUrl(): string{
if($this->_ThumbUrl === null){
if($this->ArtworkId === null){
throw new Exceptions\ArtworkNotFoundException();
}
$this->_ThumbUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb.jpg';
}
return $this->_ThumbUrl;
}
protected function GetThumb2xUrl(): string{
if($this->_Thumb2xUrl === null){
if($this->ArtworkId === null){
throw new Exceptions\ArtworkNotFoundException();
}
$this->_Thumb2xUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb@2x.jpg';
}
return $this->_Thumb2xUrl;
}
protected function GetDimensions(): string{
$this->_Dimensions = '';
try{
list($imageWidth, $imageHeight) = getimagesize(WEB_ROOT . $this->ImageUrl);
if($imageWidth && $imageHeight){
$this->_Dimensions = $imageWidth . ' × ' . $imageHeight;
}
}
catch(Exception){
// Image doesn't exist, return blank strin
}
return $this->_Dimensions;
}
protected function GetEbook(): ?Ebook{
if($this->_Ebook === null){
$this->_Ebook = Library::GetEbook($this->EbookWwwFilesystemPath);
}
return $this->_Ebook;
}
protected function SetMimeType(null|string|ImageMimeType $mimeType): void{
if(is_string($mimeType)){
$this->_MimeType = ImageMimeType::tryFrom($mimeType);
}
else{
$this->_MimeType = $mimeType;
}
}
// *******
// METHODS
// *******
/**
* @param array<mixed> $uploadedFile
* @throws \Exceptions\ValidationException
*/
protected function Validate(array &$uploadedFile = []): void{
$error = new Exceptions\ValidationException();
if($this->Artist === null){
$error->Add(new Exceptions\InvalidArtworkException());
}
try{
$this->Artist->Validate();
}
catch(Exceptions\ValidationException $ex){
$error->Add($ex);
}
if($this->Exception !== null && trim($this->Exception) == ''){
$this->Exception = null;
}
if($this->Name === null || $this->Name == ''){
$error->Add(new Exceptions\ArtworkNameRequiredException());
}
if($this->Name !== null && strlen($this->Name) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Artwork Name'));
}
if($this->CompletedYear !== null && ($this->CompletedYear <= 0 || $this->CompletedYear > intval(date('Y')))){
$error->Add(new Exceptions\InvalidCompletedYearException());
}
if($this->CompletedYear === null && $this->CompletedYearIsCirca){
$this->CompletedYearIsCirca = false;
}
if($this->PublicationYear !== null && ($this->PublicationYear <= 0 || $this->PublicationYear > intval(date('Y')))){
$error->Add(new Exceptions\InvalidPublicationYearException());
}
if($this->Status !== null && !in_array($this->Status, [COVER_ARTWORK_STATUS_UNVERIFIED, COVER_ARTWORK_STATUS_APPROVED, COVER_ARTWORK_STATUS_DECLINED, COVER_ARTWORK_STATUS_IN_USE])){
$error->Add(new Exceptions\InvalidArtworkException('Invalid status.'));
}
if($this->Status === COVER_ARTWORK_STATUS_IN_USE && $this->EbookWwwFilesystemPath === null){
$error->Add(new Exceptions\MissingEbookException());
}
if($this->Tags === null || count($this->_Tags) == 0){
// In-use artwork doesn't have user-provided tags.
if($this->Status !== COVER_ARTWORK_STATUS_IN_USE){
$error->Add(new Exceptions\TagsRequiredException());
}
}
if($this->Tags !== null && count($this->_Tags) > COVER_ARTWORK_MAX_TAGS){
$error->Add(new Exceptions\TooManyTagsException());
}
foreach($this->Tags as $tag){
if(strlen($tag->Name) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Artwork Tag: '. $tag->Name));
}
}
if($this->MuseumUrl !== null && strlen($this->MuseumUrl) > 0 && filter_var($this->MuseumUrl, FILTER_VALIDATE_URL) === false){
$error->Add(new Exceptions\InvalidMuseumUrlException());
}
if($this->MuseumUrl !== null && strlen($this->MuseumUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Link to an approved museum page'));
}
if($this->PublicationYearPageUrl !== null && strlen($this->PublicationYearPageUrl) > 0 && filter_var($this->PublicationYearPageUrl, FILTER_VALIDATE_URL) === false){
$error->Add(new Exceptions\InvalidPublicationYearPageUrlException());
}
if($this->PublicationYearPageUrl !== null && strlen($this->PublicationYearPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Link to page with year of publication'));
}
if($this->CopyrightPageUrl !== null && strlen($this->CopyrightPageUrl) > 0 && filter_var($this->CopyrightPageUrl, FILTER_VALIDATE_URL) === false){
$error->Add(new Exceptions\InvalidCopyrightPageUrlException());
}
if($this->CopyrightPageUrl !== null && strlen($this->CopyrightPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Link to page with copyright details'));
}
if($this->ArtworkPageUrl !== null && strlen($this->ArtworkPageUrl) > 0 && filter_var($this->ArtworkPageUrl, FILTER_VALIDATE_URL) === false){
$error->Add(new Exceptions\InvalidArtworkPageUrlException());
}
if($this->ArtworkPageUrl !== null && strlen($this->ArtworkPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Link to page with artwork'));
}
$hasMuseumProof = $this->MuseumUrl !== null && $this->MuseumUrl != '';
$hasBookProof = $this->PublicationYear !== null
&& ($this->PublicationYearPageUrl !== null && $this->PublicationYearPageUrl != '')
&& ($this->ArtworkPageUrl !== null && $this->ArtworkPageUrl != '');
if(!$hasMuseumProof && !$hasBookProof && $this->Exception === null){
$error->Add(new Exceptions\MissingPdProofException());
}
if($this->MimeType === null){
$error->Add(new Exceptions\InvalidMimeTypeException());
}
// Check the ebook www filesystem path.
// We don't check if it exists, because the book might not be published yet.
// But we do a basic check that the string includes one _. It might not include a dash, for example anonymous_poetry
if($this->EbookWwwFilesystemPath !== null){
if(mb_stripos($this->EbookWwwFilesystemPath, '_') === false){
$error->Add(new Exceptions\InvalidEbookException('Invalid ebook. Expected file system slug like “c-s-lewis_poetry”.'));
}
}
// Check for existing Artwork objects with the same URL but different Artwork IDs.
try{
$existingArtwork = Artwork::GetByUrl($this->Artist->UrlName, $this->UrlName);
if($existingArtwork->ArtworkId != $this->ArtworkId){
// Duplicate found, alert the user
$error->Add(new Exceptions\ArtworkAlreadyExistsException());
}
}
catch(Exceptions\ArtworkNotFoundException){
// No duplicates found, continue
}
if(!is_writable(WEB_ROOT . COVER_ART_UPLOAD_PATH)){
$error->Add(new Exceptions\InvalidImageUploadException('Upload path not writable.'));
}
if(!empty($uploadedFile) && $this->MimeType !== null){
$uploadError = $uploadedFile['error'];
if($uploadError > UPLOAD_ERR_OK){
// see https://www.php.net/manual/en/features.file-upload.errors.php
$message = match($uploadError){
UPLOAD_ERR_INI_SIZE => 'Image upload too large (maximum ' . ini_get('upload_max_filesize') . ').',
default => 'Image failed to upload (error code ' . $uploadError . ').',
};
$error->Add(new Exceptions\InvalidImageUploadException($message));
}
if(!is_uploaded_file($uploadedFile['tmp_name'])){
$error->Add(new Exceptions\InvalidImageUploadException());
}
// Check for minimum dimensions
list($imageWidth, $imageHeight) = getimagesize($uploadedFile['tmp_name']);
if(!$imageWidth || !$imageHeight || $imageWidth < COVER_ARTWORK_IMAGE_MINIMUM_WIDTH || $imageHeight < COVER_ARTWORK_IMAGE_MINIMUM_HEIGHT){
$error->Add(new Exceptions\ArtworkImageDimensionsTooSmallException());
}
}
if($error->HasExceptions){
throw $error;
}
}
/** @return array<ArtworkTag> */
public static function ParseTags(?string $tags): array{
if(!$tags) return [];
$tags = array_map('trim', explode(',', $tags));
$tags = array_values(array_filter($tags));
$tags = array_unique($tags);
return array_map(function ($str){
$tag = new ArtworkTag();
$tag->Name = $str;
return $tag;
}, $tags);
}
/**
* @param array<mixed> $uploadedFile
* @throws \Exceptions\ValidationException
* @throws \Exceptions\InvalidImageUploadException
*/
public function Create(array $uploadedFile): void{
$this->Validate($uploadedFile);
$this->Created = new DateTime();
// Can't assign directly to $this->Tags because it's hidden behind a getter
$tags = [];
foreach($this->Tags as $artworkTag){
$tags[] = ArtworkTag::GetOrCreate($artworkTag);
}
$this->Tags = $tags;
$this->Artist = Artist::GetOrCreate($this->Artist);
Db::Query('
INSERT into
Artworks (ArtistId, Name, UrlName, CompletedYear, CompletedYearIsCirca, Created, Status, SubmitterUserId, ReviewerUserId, MuseumUrl,
PublicationYear, PublicationYearPageUrl, CopyrightPageUrl, ArtworkPageUrl, IsPublishedInUs,
EbookWwwFilesystemPath, MimeType, Exception)
values (?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?)
', [$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->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookWwwFilesystemPath, $this->MimeType->value ?? null, $this->Exception]
);
$this->ArtworkId = Db::GetLastInsertedId();
foreach($this->Tags as $tag){
Db::Query('
INSERT into ArtworkTags (ArtworkId, TagId)
values (?,
?)
', [$this->ArtworkId, $tag->TagId]);
}
// Save the source image and clean up metadata
$imageUploadPath = $uploadedFile['tmp_name'];
exec('exiftool -quiet -overwrite-_original -all= ' . escapeshellarg($imageUploadPath));
copy($imageUploadPath, WEB_ROOT . $this->ImageUrl);
// Generate the thumbnails
try{
$image = new Image($imageUploadPath);
$image->Resize(WEB_ROOT . $this->ThumbUrl, COVER_THUMBNAIL_WIDTH, COVER_THUMBNAIL_HEIGHT);
$image->Resize(WEB_ROOT . $this->Thumb2xUrl, COVER_THUMBNAIL_WIDTH * 2, COVER_THUMBNAIL_HEIGHT * 2);
}
catch(\Safe\Exceptions\FilesystemException | \Safe\Exceptions\ImageException){
throw new Exceptions\InvalidImageUploadException('Failed to generate thumbnail.');
}
}
/**
* @throws \Exceptions\ValidationException
*/
public function Save(): void{
$this->Validate();
Db::Query('
UPDATE Artworks
set
ArtistId = ?,
Name = ?,
UrlName = ?,
CompletedYear = ?,
CompletedYearIsCirca = ?,
Created = ?,
Status = ?,
SubmitterUserId = ?,
ReviewerUserId = ?,
MuseumUrl = ?,
PublicationYear = ?,
PublicationYearPageUrl = ?,
CopyrightPageUrl = ?,
ArtworkPageUrl = ?,
IsPublishedInUs = ?,
EbookWwwFilesystemPath = ?,
MimeType = ?,
Exception = ?
where
ArtworkId = ?
', [$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->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookWwwFilesystemPath, $this->MimeType->value ?? null, $this->Exception,
$this->ArtworkId]
);
}
public function MarkInUse(string $ebookWwwFilesystemPath): void{
$this->EbookWwwFilesystemPath = $ebookWwwFilesystemPath;
$this->Status = COVER_ARTWORK_STATUS_IN_USE;
$this->Save();
}
public function Delete(): void{
Db::Query('
DELETE
from ArtworkTags
where ArtworkId = ?
', [$this->ArtworkId]);
Db::Query('
DELETE
from Artworks
where ArtworkId = ?
', [$this->ArtworkId]);
}
public function Contains(string $query): bool{
$searchString = $this->Name;
$searchString .= ' ' . $this->Artist->Name;
$searchString .= ' ' . implode(' ', $this->Artist->AlternateSpellings);
foreach($this->Tags as $tag){
$searchString .= ' ' . $tag->Name;
}
// Remove diacritics and non-alphanumeric characters
$searchString = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($searchString)));
$query = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($query)));
if($query == ''){
return false;
}
if(mb_stripos($searchString, $query) !== false){
return true;
}
return false;
}
// ***********
// ORM METHODS
// ***********
/**
* @throws \Exceptions\ArtworkNotFoundException
*/
public static function Get(?int $artworkId): Artwork{
if($artworkId === null){
throw new Exceptions\ArtworkNotFoundException();
}
$result = Db::Query('
SELECT *
from Artworks
where ArtworkId = ?
', [$artworkId], 'Artwork');
if(sizeof($result) == 0){
throw new Exceptions\ArtworkNotFoundException();
}
return $result[0];
}
/**
* Looks up an existing artwork regardless of status (unlike GetByUrl()) in order to
* enforce that the Artist UrlName + Artwork UrlName combo is globally unique.
*/
/**
* @throws \Exceptions\InvalidArtworkException
*/
public static function GetByUrl(?string $artistUrlName, ?string $artworkUrlName): Artwork{
if($artistUrlName === null || $artworkUrlName === null){
throw new Exceptions\ArtworkNotFoundException();
}
$result = Db::Query('
SELECT Artworks.*
from Artworks
inner join Artists using (ArtistId)
where Artists.UrlName = ? and Artworks.UrlName = ?
', [$artistUrlName, $artworkUrlName], 'Artwork');
if(sizeof($result) == 0){
throw new Exceptions\ArtworkNotFoundException();
}
return $result[0];
}
}