Update PropertiesBase to new patterns and improve static analysis checks

This commit is contained in:
Alex Cabal 2022-06-30 13:23:05 -05:00
parent 5f0b57f7e9
commit 6c8414f844
33 changed files with 335 additions and 148 deletions

View file

@ -11,22 +11,16 @@ parameters:
# Ignore errors caused by no type hints on class properties, as that's not available till PHP 7.4 # Ignore errors caused by no type hints on class properties, as that's not available till PHP 7.4
- '#Property .+? has no type specified.#' - '#Property .+? has no type specified.#'
# Ignore errors caused by missing phpdoc strings for arrays
- '#Method .+? has parameter .+? with no value type specified in iterable type array.#'
# Ignore errors caused by type hints that should be union types. Union types are not yet supported in PHP. # Ignore errors caused by type hints that should be union types. Union types are not yet supported in PHP.
- '#Function vd(s|d)?\(\) has parameter \$var with no type specified.#' - '#Function vd(s|d)?\(\) has parameter \$var with no type specified.#'
- '#Method Ebook::NullIfEmpty\(\) has parameter \$elements with no type specified.#' - '#Method Ebook::NullIfEmpty\(\) has parameter \$elements with no type specified.#'
- '#Method HttpInput::GetHttpVar\(\) has no return type specified.#' - '#Method HttpInput::GetHttpVar\(\) has no return type specified.#'
- '#Method HttpInput::GetHttpVar\(\) has parameter \$default with no type specified.#' - '#Method HttpInput::GetHttpVar\(\) has parameter \$default with no type specified.#'
# Ignore errors caused by access to our PropertiesBase pattern
- '#Access to protected property .+#'
# Ignore symbols that PHPStan can't find # Ignore symbols that PHPStan can't find
- '#Constant EMAIL_SMTP_USERNAME not found.#' - '#Constant EMAIL_SMTP_USERNAME not found.#'
level: level:
7 8
paths: paths:
- %rootDir%/../../../lib - %rootDir%/../../../lib
- %rootDir%/../../../www - %rootDir%/../../../www

View file

@ -1,7 +0,0 @@
<?
require_once('/standardebooks.org/web/lib/Core.php');
//file_get_contents('/home/alex/donations.csv');
$csv = array_map( 'str_getcsv', file( '/home/alex/donations.csv') );
vdd($csv);

View file

@ -12,6 +12,13 @@ class AtomFeed extends Feed{
public $Updated = null; public $Updated = null;
public $Subtitle = null; public $Subtitle = null;
/**
* @param string $title
* @param string $subtitle
* @param string $url
* @param string $path
* @param array<Ebook> $entries
*/
public function __construct(string $title, string $subtitle, string $url, string $path, array $entries){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries){
parent::__construct($title, $url, $path, $entries); parent::__construct($title, $url, $path, $entries);
$this->Subtitle = $subtitle; $this->Subtitle = $subtitle;
@ -19,6 +26,11 @@ class AtomFeed extends Feed{
$this->Stylesheet = '/feeds/atom/style'; $this->Stylesheet = '/feeds/atom/style';
} }
// *******
// METHODS
// *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if($this->XmlString === null){
$feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'subtitle' => $this->Subtitle, 'updated' => $this->Updated, 'entries' => $this->Entries]); $feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'subtitle' => $this->Subtitle, 'updated' => $this->Updated, 'entries' => $this->Entries]);

View file

@ -6,6 +6,9 @@ class Db{
} }
/** /**
* @param string $query
* @param array<mixed> $args
* @param string $class
* @return Array<mixed> * @return Array<mixed>
*/ */
public static function Query(string $query, array $args = [], string $class = 'stdClass'): array{ public static function Query(string $query, array $args = [], string $class = 'stdClass'): array{
@ -20,6 +23,10 @@ class Db{
return $GLOBALS['DbConnection']->Query($query, $args, $class); return $GLOBALS['DbConnection']->Query($query, $args, $class);
} }
/**
* @param string $query
* @param array<mixed> $args
*/
public static function QueryInt(string $query, array $args = []): int{ public static function QueryInt(string $query, array $args = []): int{
// Useful for queries that return a single integer as a result, like count(*) or sum(*). // Useful for queries that return a single integer as a result, like count(*) or sum(*).

View file

@ -72,6 +72,9 @@ class DbConnection{
// array $params = an array of parameters to bind to the SQL statement // array $params = an array of parameters to bind to the SQL statement
// Returns: a resource record or null on error // Returns: a resource record or null on error
/** /**
* @param string $sql
* @param array<mixed> $params
* @param string $class
* @return Array<mixed> * @return Array<mixed>
*/ */
public function Query(string $sql, array $params = [], string $class = 'stdClass'): array{ public function Query(string $sql, array $params = [], string $class = 'stdClass'): array{

View file

@ -184,12 +184,12 @@ class Ebook{
$this->AlternateTitle = $this->NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:alternate-title"]')); $this->AlternateTitle = $this->NullIfEmpty($xml->xpath('/package/metadata/meta[@property="se:alternate-title"]'));
$date = $xml->xpath('/package/metadata/dc:date'); $date = $xml->xpath('/package/metadata/dc:date') ?: [];
if($date !== false && sizeof($date) > 0){ if($date !== false && sizeof($date) > 0){
$this->Created = new DateTime((string)$date[0]); $this->Created = new DateTime((string)$date[0]);
} }
$modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]'); $modifiedDate = $xml->xpath('/package/metadata/meta[@property="dcterms:modified"]') ?: [];
if($modifiedDate !== false && sizeof($modifiedDate) > 0){ if($modifiedDate !== false && sizeof($modifiedDate) > 0){
$this->Updated = new DateTime((string)$modifiedDate[0]); $this->Updated = new DateTime((string)$modifiedDate[0]);
} }
@ -214,10 +214,12 @@ class Ebook{
// Get SE collections // Get SE collections
foreach($xml->xpath('/package/metadata/meta[@property="belongs-to-collection"]') ?: [] as $collection){ foreach($xml->xpath('/package/metadata/meta[@property="belongs-to-collection"]') ?: [] as $collection){
$c = new Collection($collection); $c = new Collection($collection);
foreach($xml->xpath('/package/metadata/meta[@refines="#' . $collection->attributes()->id . '"][@property="group-position"]') ?: [] as $s){ $id = $collection->attributes()->id ?? '';
foreach($xml->xpath('/package/metadata/meta[@refines="#' . $id . '"][@property="group-position"]') ?: [] as $s){
$c->SequenceNumber = (int)$s; $c->SequenceNumber = (int)$s;
} }
foreach($xml->xpath('/package/metadata/meta[@refines="#' . $collection->attributes()->id . '"][@property="collection-type"]') ?: [] as $s){ foreach($xml->xpath('/package/metadata/meta[@refines="#' . $id . '"][@property="collection-type"]') ?: [] as $s){
$c->Type = (string)$s; $c->Type = (string)$s;
} }
$this->Collections[] = $c; $this->Collections[] = $c;
@ -237,7 +239,7 @@ class Ebook{
} }
$fileAs = null; $fileAs = null;
$fileAsElement = $xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]'); $fileAsElement = $xml->xpath('/package/metadata/meta[@property="file-as"][@refines="#' . $id . '"]') ?: [];
if($fileAsElement !== false && sizeof($fileAsElement) > 0){ if($fileAsElement !== false && sizeof($fileAsElement) > 0){
$fileAs = (string)$fileAsElement[0]; $fileAs = (string)$fileAsElement[0];
} }
@ -449,6 +451,11 @@ class Ebook{
$this->TitleWithCreditsHtml = Formatter::ToPlainText($this->Title) . ', by ' . str_replace('&amp;', '&', $this->AuthorsHtml . $titleContributors); $this->TitleWithCreditsHtml = Formatter::ToPlainText($this->Title) . ', by ' . str_replace('&amp;', '&', $this->AuthorsHtml . $titleContributors);
} }
// *******
// METHODS
// *******
public function GetCollectionPosition(Collection $collection): ?int{ public function GetCollectionPosition(Collection $collection): ?int{
foreach($this->Collections as $c){ foreach($this->Collections as $c){
if($c->Name == $collection->Name){ if($c->Name == $collection->Name){

View file

@ -18,6 +18,19 @@ class Email{
public $Attachments = array(); public $Attachments = array();
public $PostmarkStream = null; public $PostmarkStream = null;
public function __construct(bool $isNoReplyEmail = false){
if($isNoReplyEmail){
$this->From = NO_REPLY_EMAIL_ADDRESS;
$this->FromName = 'Standard Ebooks';
$this->ReplyTo = NO_REPLY_EMAIL_ADDRESS;
}
}
// *******
// METHODS
// *******
public function Send(): bool{ public function Send(): bool{
if($this->ReplyTo == ''){ if($this->ReplyTo == ''){
$this->ReplyTo = $this->From; $this->ReplyTo = $this->From;
@ -82,12 +95,4 @@ class Email{
return true; return true;
} }
public function __construct(bool $isNoReplyEmail = false){
if($isNoReplyEmail){
$this->From = NO_REPLY_EMAIL_ADDRESS;
$this->FromName = 'Standard Ebooks';
$this->ReplyTo = NO_REPLY_EMAIL_ADDRESS;
}
}
} }

View file

@ -13,6 +13,12 @@ class Feed{
public $Stylesheet = null; public $Stylesheet = null;
protected $XmlString = null; protected $XmlString = null;
/**
* @param string $title
* @param string $url
* @param string $path
* @param array<Ebook> $entries
*/
public function __construct(string $title, string $url, string $path, array $entries){ public function __construct(string $title, string $url, string $path, array $entries){
$this->Url = $url; $this->Url = $url;
$this->Title = $title; $this->Title = $title;
@ -20,6 +26,11 @@ class Feed{
$this->Entries = $entries; $this->Entries = $entries;
} }
// *******
// METHODS
// *******
protected function CleanXmlString(string $xmlString): string{ protected function CleanXmlString(string $xmlString): string{
$tempFilename = tempnam('/tmp/', 'se-'); $tempFilename = tempnam('/tmp/', 'se-');
file_put_contents($tempFilename, $xmlString); file_put_contents($tempFilename, $xmlString);

View file

@ -33,10 +33,10 @@ class Formatter{
} }
public static function ToPlainText(?string $text): string{ public static function ToPlainText(?string $text): string{
return htmlspecialchars(trim($text), ENT_QUOTES, 'utf-8'); return htmlspecialchars(trim($text ?? ''), ENT_QUOTES, 'utf-8');
} }
public static function ToPlainXmlText(?string $text): string{ public static function ToPlainXmlText(?string $text): string{
return htmlspecialchars(trim($text), ENT_QUOTES|ENT_XML1, 'utf-8'); return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8');
} }
} }

View file

@ -52,6 +52,8 @@ class HttpInput{
} }
/** /**
* @param string $variable
* @param array<mixed> $default
* @return array<string> * @return array<string>
*/ */
public static function GetArray(string $variable, array $default = null): ?array{ public static function GetArray(string $variable, array $default = null): ?array{

View file

@ -8,6 +8,9 @@ use function Safe\usort;
class Library{ class Library{
/** /**
* @param string $query
* @param array<string> $tags
* @param string $sort
* @return array<Ebook> * @return array<Ebook>
*/ */
public static function FilterEbooks(string $query = null, array $tags = [], string $sort = null){ public static function FilterEbooks(string $query = null, array $tags = [], string $sort = null){

View file

@ -16,6 +16,11 @@ class Log{
$this->LogFilePath = $logFilePath; $this->LogFilePath = $logFilePath;
} }
// *******
// METHODS
// *******
public function Write(string $text): void{ public function Write(string $text): void{
if($this->LogFilePath === null){ if($this->LogFilePath === null){
self::WriteErrorLogEntry($text); self::WriteErrorLogEntry($text);

View file

@ -2,6 +2,9 @@
use Safe\DateTime; use Safe\DateTime;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
/**
* @property string $Url
*/
class NewsletterSubscriber extends PropertiesBase{ class NewsletterSubscriber extends PropertiesBase{
public $NewsletterSubscriberId; public $NewsletterSubscriberId;
public $Uuid; public $Uuid;
@ -14,6 +17,10 @@ class NewsletterSubscriber extends PropertiesBase{
public $Created; public $Created;
protected $Url = null; protected $Url = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->Url === null){ if($this->Url === null){
$this->Url = SITE_URL . '/newsletter/subscribers/' . $this->Uuid; $this->Url = SITE_URL . '/newsletter/subscribers/' . $this->Uuid;
@ -22,6 +29,11 @@ class NewsletterSubscriber extends PropertiesBase{
return $this->Url; return $this->Url;
} }
// *******
// METHODS
// *******
public function Create(): void{ public function Create(): void{
$this->Validate(); $this->Validate();
@ -78,6 +90,11 @@ class NewsletterSubscriber extends PropertiesBase{
} }
} }
// ***********
// ORM METHODS
// ***********
public static function Get(string $uuid): NewsletterSubscriber{ public static function Get(string $uuid): NewsletterSubscriber{
$subscribers = Db::Query('SELECT * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber'); $subscribers = Db::Query('SELECT * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');

View file

@ -9,6 +9,11 @@ class OpdsAcquisitionFeed extends OpdsFeed{
$this->IsCrawlable = $isCrawlable; $this->IsCrawlable = $isCrawlable;
} }
// *******
// METHODS
// *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ 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])); $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]));

View file

@ -5,12 +5,25 @@ use function Safe\file_put_contents;
class OpdsFeed extends AtomFeed{ class OpdsFeed extends AtomFeed{
public $Parent = null; // OpdsNavigationFeed class public $Parent = null; // OpdsNavigationFeed class
/**
* @param string $title
* @param string $subtitle
* @param string $url
* @param string $path
* @param array<Ebook> $entries
* @param OpdsNavigationFeed $parent
*/
public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){
parent::__construct($title, $subtitle, $url, $path, $entries); parent::__construct($title, $subtitle, $url, $path, $entries);
$this->Parent = $parent; $this->Parent = $parent;
$this->Stylesheet = '/feeds/opds/style'; $this->Stylesheet = '/feeds/opds/style';
} }
// *******
// METHODS
// *******
protected function SaveUpdated(string $entryId, DateTime $updated): void{ protected function SaveUpdated(string $entryId, DateTime $updated): void{
// Only save the updated timestamp for the given entry ID in this file // Only save the updated timestamp for the given entry ID in this file
foreach($this->Entries as $entry){ foreach($this->Entries as $entry){

View file

@ -4,6 +4,14 @@ use Safe\DateTime;
use function Safe\file_get_contents; use function Safe\file_get_contents;
class OpdsNavigationFeed extends OpdsFeed{ class OpdsNavigationFeed extends OpdsFeed{
/**
* @param string $title
* @param string $subtitle
* @param string $url
* @param string $path
* @param array<Ebook> $entries
* @param OpdsNavigationFeed $parent
*/
public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){
parent::__construct($title, $subtitle, $url, $path, $entries, $parent); parent::__construct($title, $subtitle, $url, $path, $entries, $parent);
@ -29,6 +37,11 @@ class OpdsNavigationFeed extends OpdsFeed{
} }
} }
// *******
// METHODS
// *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ 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])); $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]));

View file

@ -1,15 +1,51 @@
<? <?
use Safe\DateTime; use Safe\DateTime;
/**
* @property User $User
*/
class Patron extends PropertiesBase{ class Patron extends PropertiesBase{
protected $User = null;
public $UserId = null; public $UserId = null;
protected $User = null;
public $IsAnonymous; public $IsAnonymous;
public $AlternateName; public $AlternateName;
public $IsSubscribedToEmails; public $IsSubscribedToEmails;
public $Created = null; public $Created = null;
public $Ended = null; public $Ended = null;
// *******
// METHODS
// *******
public function Create(): void{
$this->Created = new DateTime();
Db::Query('INSERT into Patrons (Created, UserId, IsAnonymous, AlternateName, IsSubscribedToEmails) values(?, ?, ?, ?, ?);', [$this->Created, $this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmails]);
// If this is a patron for the first time, send the first-time patron email.
// Otherwise, send the returning patron email.
$isReturning = Db::QueryInt('SELECT count(*) from Patrons where UserId = ?', [$this->UserId]) > 1;
$this->SendWelcomeEmail($isReturning);
}
private function SendWelcomeEmail(bool $isReturning): void{
if($this->User !== null){
$em = new Email();
$em->To = $this->User->Email;
$em->From = EDITOR_IN_CHIEF_EMAIL_ADDRESS;
$em->Subject = 'Thank you for supporting Standard Ebooks!';
$em->Body = Template::EmailPatronsCircleWelcome(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]);
$em->TextBody = Template::EmailPatronsCircleWelcomeText(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]);
$em->Send();
}
}
// ***********
// ORM METHODS
// ***********
public static function Get(?int $userId): Patron{ public static function Get(?int $userId): Patron{
$result = Db::Query('SELECT * from Patrons where UserId = ?', [$userId], 'Patron'); $result = Db::Query('SELECT * from Patrons where UserId = ?', [$userId], 'Patron');
@ -29,28 +65,4 @@ class Patron extends PropertiesBase{
return $result[0]; return $result[0];
} }
public function Create(): void{
$this->Created = new DateTime();
Db::Query('INSERT into Patrons (Created, UserId, IsAnonymous, AlternateName, IsSubscribedToEmails) values(?, ?, ?, ?, ?);', [$this->Created, $this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmails]);
// If this is a patron for the first time, send the first-time patron email.
// Otherwise, send the returning patron email.
$isReturning = Db::QueryInt('SELECT count(*) from Patrons where UserId = ?', [$this->UserId]) > 1;
$this->SendWelcomeEmail($isReturning);
}
private function SendWelcomeEmail(bool $isReturning): void{
$this->__get('User');
if($this->User !== null){
$em = new Email();
$em->To = $this->User->Email;
$em->From = EDITOR_IN_CHIEF_EMAIL_ADDRESS;
$em->Subject = 'Thank you for supporting Standard Ebooks!';
$em->Body = Template::EmailPatronsCircleWelcome(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]);
$em->TextBody = Template::EmailPatronsCircleWelcomeText(['isAnonymous' => $this->IsAnonymous, 'isReturning' => $isReturning]);
$em->Send();
}
}
} }

View file

@ -1,9 +1,12 @@
<? <?
/**
* @property User $User
*/
class Payment extends PropertiesBase{ class Payment extends PropertiesBase{
public $PaymentId; public $PaymentId;
protected $User = null;
public $UserId = null; public $UserId = null;
protected $_User = null;
public $Created; public $Created;
public $ChannelId; public $ChannelId;
public $TransactionId; public $TransactionId;
@ -11,6 +14,11 @@ class Payment extends PropertiesBase{
public $Fee; public $Fee;
public $IsRecurring; public $IsRecurring;
// *******
// METHODS
// *******
public function Create(): void{ public function Create(): void{
if($this->UserId === null){ if($this->UserId === null){
// Check if we have to create a new user in the database // Check if we have to create a new user in the database

View file

@ -2,6 +2,12 @@
use Safe\DateTime; use Safe\DateTime;
use function Safe\usort; use function Safe\usort;
/**
* @property string $Url
* @property array<PollItem> $PollItems
* @property array<PollItem> $PollItemsByWinner
* @property int $VoteCount
*/
class Poll extends PropertiesBase{ class Poll extends PropertiesBase{
public $PollId; public $PollId;
public $Name; public $Name;
@ -10,53 +16,62 @@ class Poll extends PropertiesBase{
public $Created; public $Created;
public $Start; public $Start;
public $End; public $End;
protected $Url = null; protected $_Url = null;
protected $PollItems = null; protected $_PollItems = null;
protected $PollItemsByWinner = null; protected $_PollItemsByWinner = null;
protected $VoteCount = null; protected $_VoteCount = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->Url === null){ if($this->_Url === null){
$this->Url = '/patrons-circle/polls/' . $this->UrlName; $this->_Url = '/patrons-circle/polls/' . $this->UrlName;
} }
return $this->Url; return $this->_Url;
} }
protected function GetVoteCount(): int{ protected function GetVoteCount(): int{
if($this->VoteCount === null){ if($this->_VoteCount === null){
$this->VoteCount = Db::QueryInt('select count(*) from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollId = ?', [$this->PollId]); $this->_VoteCount = Db::QueryInt('select count(*) from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollId = ?', [$this->PollId]);
} }
return $this->VoteCount; return $this->_VoteCount;
} }
/** /**
* @return array<PollItem> * @return array<PollItem>
*/ */
protected function GetPollItems(): array{ protected function GetPollItems(): array{
if($this->PollItems === null){ if($this->_PollItems === null){
$this->PollItems = Db::Query('SELECT * from PollItems where PollId = ? order by SortOrder asc', [$this->PollId], 'PollItem'); $this->_PollItems = Db::Query('SELECT * from PollItems where PollId = ? order by SortOrder asc', [$this->PollId], 'PollItem');
} }
return $this->PollItems; return $this->_PollItems;
} }
/** /**
* @return array<PollItem> * @return array<PollItem>
*/ */
protected function GetPollItemsByWinner(): array{ protected function GetPollItemsByWinner(): array{
if($this->PollItemsByWinner === null){ if($this->_PollItemsByWinner === null){
$this->__get('PollItems'); $this->_PollItemsByWinner = $this->PollItems;
$this->PollItemsByWinner = $this->PollItems; usort($this->_PollItemsByWinner, function(PollItem $a, PollItem $b){ return $a->VoteCount <=> $b->VoteCount; });
usort($this->PollItemsByWinner, function(PollItem $a, PollItem $b){ return $a->VoteCount <=> $b->VoteCount; });
$this->PollItemsByWinner = array_reverse($this->PollItemsByWinner); $this->_PollItemsByWinner = array_reverse($this->_PollItemsByWinner);
} }
return $this->PollItemsByWinner; return $this->_PollItemsByWinner;
} }
// *******
// METHODS
// *******
public function IsActive(): bool{ public function IsActive(): bool{
$now = new DateTime(); $now = new DateTime();
if( ($this->Start !== null && $this->Start > $now) || ($this->End !== null && $this->End < $now)){ if( ($this->Start !== null && $this->Start > $now) || ($this->End !== null && $this->End < $now)){
@ -66,6 +81,11 @@ class Poll extends PropertiesBase{
return true; return true;
} }
// ***********
// ORM METHODS
// ***********
public static function Get(?int $pollId): Poll{ public static function Get(?int $pollId): Poll{
$result = Db::Query('SELECT * from Polls where PollId = ?', [$pollId], 'Poll'); $result = Db::Query('SELECT * from Polls where PollId = ?', [$pollId], 'Poll');

View file

@ -1,20 +1,35 @@
<? <?
/**
* @property int $VoteCount
* @property Poll $Poll
*/
class PollItem extends PropertiesBase{ class PollItem extends PropertiesBase{
public $PollItemId; public $PollItemId;
public $PollId; public $PollId;
public $Name; public $Name;
public $Description; public $Description;
protected $VoteCount = null; protected $_VoteCount = null;
protected $Poll = null; protected $_Poll = null;
// *******
// GETTERS
// *******
protected function GetVoteCount(): int{ protected function GetVoteCount(): int{
if($this->VoteCount === null){ if($this->_VoteCount === null){
$this->VoteCount = Db::QueryInt('select count(*) from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollItemId = ?', [$this->PollItemId]); $this->_VoteCount = Db::QueryInt('select count(*) from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollItemId = ?', [$this->PollItemId]);
} }
return $this->VoteCount; return $this->_VoteCount;
} }
// ***********
// ORM METHODS
// ***********
public static function Get(?int $pollItemId): PollItem{ public static function Get(?int $pollItemId): PollItem{
$result = Db::Query('SELECT * from PollItems where PollItemId = ?', [$pollItemId], 'PollItem'); $result = Db::Query('SELECT * from PollItems where PollItemId = ?', [$pollItemId], 'PollItem');

View file

@ -8,27 +8,24 @@ abstract class PropertiesBase{
*/ */
public function __get($var){ public function __get($var){
$function = 'Get' . $var; $function = 'Get' . $var;
$privateVar = '_' . $var;
if(method_exists($this, $function)){ if(method_exists($this, $function)){
return $this->$function(); return $this->$function();
} }
elseif(property_exists($this, $var . 'Id') && method_exists($var, 'Get')){ elseif(property_exists($this, $var . 'Id') && property_exists($this, $privateVar) && method_exists($var, 'Get')){
// If our object has an VarId attribute, and the Var class also has a ::Get method, // If we're asking for a private `_Var` property,
// call it and return the result // and we have a public `VarId` property,
if($this->$var === null && $this->{$var . 'Id'} !== null){ // and the `Var` class also has a `Var::Get` method,
$this->$var = $var::Get($this->{$var . 'Id'}); // call that method and return the result.
if($this->$privateVar === null && $this->{$var . 'Id'} !== null){
$this->$privateVar = $var::Get($this->{$var . 'Id'});
} }
return $this->$var; return $this->$privateVar;
} }
elseif(substr($var, 0, 7) == 'Display'){ elseif(property_exists($this, $privateVar)){
// If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML. return $this->{$privateVar};
if($this->$var === null){
$target = substr($var, 7, strlen($var));
$this->$var = Formatter::ToPlainText($this->$target);
}
return $this->$var;
} }
else{ else{
return $this->$var; return $this->$var;
@ -41,9 +38,14 @@ abstract class PropertiesBase{
*/ */
public function __set(string $var, $val){ public function __set(string $var, $val){
$function = 'Set' . $var; $function = 'Set' . $var;
$privateVar = '_' . $var;
if(method_exists($this, $function)){ if(method_exists($this, $function)){
$this->$function($val); $this->$function($val);
} }
elseif(property_exists($this, $privateVar)){
$this->$privateVar = $val;
}
else{ else{
$this->$var = $val; $this->$var = $val;
} }

View file

@ -7,12 +7,24 @@ use function Safe\preg_replace;
class RssFeed extends Feed{ class RssFeed extends Feed{
public $Description; public $Description;
/**
* @param string $title
* @param string $description
* @param string $url
* @param string $path
* @param array<Ebook> $entries
*/
public function __construct(string $title, string $description, string $url, string $path, array $entries){ public function __construct(string $title, string $description, string $url, string $path, array $entries){
parent::__construct($title, $url, $path, $entries); parent::__construct($title, $url, $path, $entries);
$this->Description = $description; $this->Description = $description;
$this->Stylesheet = '/feeds/rss/style'; $this->Stylesheet = '/feeds/rss/style';
} }
// *******
// METHODS
// *******
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if($this->XmlString === null){
$feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => (new DateTime())->format('r')]); $feed = Template::RssFeed(['url' => $this->Url, 'description' => $this->Description, 'title' => $this->Title, 'entries' => $this->Entries, 'updated' => (new DateTime())->format('r')]);

View file

@ -2,6 +2,10 @@
use function Safe\ob_end_clean; use function Safe\ob_end_clean;
class Template{ class Template{
/**
* @param string $templateName
* @param array<mixed> $arguments
*/
protected static function Get(string $templateName, array $arguments = []): string{ protected static function Get(string $templateName, array $arguments = []): string{
// Expand the passed variables to make them available to the included template. // Expand the passed variables to make them available to the included template.
// We use these funny names so that we can use 'name' and 'value' as template variables if we want to. // We use these funny names so that we can use 'name' and 'value' as template variables if we want to.
@ -17,6 +21,10 @@ class Template{
return $contents; return $contents;
} }
/**
* @param string $function
* @param array<mixed> $arguments
*/
public static function __callStatic(string $function, array $arguments): string{ public static function __callStatic(string $function, array $arguments): string{
if(isset($arguments[0])){ if(isset($arguments[0])){
return self::Get($function, $arguments[0]); return self::Get($function, $arguments[0]);

View file

@ -5,35 +5,16 @@ use Safe\DateTime;
class User extends PropertiesBase{ class User extends PropertiesBase{
public $UserId; public $UserId;
public $FirstName; public $FirstName;
protected $DisplayFirstName = null;
public $LastName; public $LastName;
protected $DisplayLastName = null;
protected $Name = null; protected $Name = null;
protected $DisplayName = null;
public $Email; public $Email;
protected $DisplayEmail;
public $Created; public $Created;
public $Uuid; public $Uuid;
public static function Get(?int $userId): User{
$result = Db::Query('SELECT * from Users where UserId = ?', [$userId], 'User');
if(sizeof($result) == 0){ // *******
throw new Exceptions\InvalidUserException(); // GETTERS
} // *******
return $result[0];
}
public static function GetByEmail(?string $email): User{
$result = Db::Query('SELECT * from Users where Email = ?', [$email], 'User');
if(sizeof($result) == 0){
throw new Exceptions\InvalidUserException();
}
return $result[0];
}
protected function GetName(): string{ protected function GetName(): string{
if($this->Name === null){ if($this->Name === null){
@ -43,6 +24,11 @@ class User extends PropertiesBase{
return $this->Name; return $this->Name;
} }
// *******
// METHODS
// *******
public function Create(): void{ public function Create(): void{
$uuid = Uuid::uuid4(); $uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString(); $this->Uuid = $uuid->toString();
@ -63,4 +49,29 @@ class User extends PropertiesBase{
$this->UserId = Db::GetLastInsertedId(); $this->UserId = Db::GetLastInsertedId();
} }
// ***********
// ORM METHODS
// ***********
public static function Get(?int $userId): User{
$result = Db::Query('SELECT * from Users where UserId = ?', [$userId], 'User');
if(sizeof($result) == 0){
throw new Exceptions\InvalidUserException();
}
return $result[0];
}
public static function GetByEmail(?string $email): User{
$result = Db::Query('SELECT * from Users where Email = ?', [$email], 'User');
if(sizeof($result) == 0){
throw new Exceptions\InvalidUserException();
}
return $result[0];
}
} }

View file

@ -1,23 +1,38 @@
<? <?
use Safe\DateTime; use Safe\DateTime;
/**
* @property User $User
* @property PollItem $PollItem
* @property string $Url
*/
class Vote extends PropertiesBase{ class Vote extends PropertiesBase{
public $VoteId; public $VoteId;
public $UserId; public $UserId;
protected $User = null; protected $_User = null;
public $Created; public $Created;
public $PollItemId; public $PollItemId;
protected $PollItem = null; protected $_PollItem = null;
protected $Url = null; protected $_Url = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if($this->Url === null){ if($this->_Url === null){
$this->Url = '/patrons-circle/polls/' . $this->PollItem->Poll->Url . '/votes/' . $this->UserId; $this->_Url = '/patrons-circle/polls/' . $this->PollItem->Poll->Url . '/votes/' . $this->UserId;
} }
return $this->Url; return $this->_Url;
} }
// *******
// METHODS
// *******
protected function Validate(): void{ protected function Validate(): void{
$error = new Exceptions\ValidationException(); $error = new Exceptions\ValidationException();
@ -29,7 +44,6 @@ class Vote extends PropertiesBase{
$error->Add(new Exceptions\PollItemRequiredException()); $error->Add(new Exceptions\PollItemRequiredException());
} }
else{ else{
$this->__get('PollItem');
if($this->PollItem === null){ if($this->PollItem === null){
$error->Add(new Exceptions\InvalidPollException()); $error->Add(new Exceptions\InvalidPollException());
} }
@ -43,7 +57,6 @@ class Vote extends PropertiesBase{
// Basic sanity checks done, now check if we've already voted // Basic sanity checks done, now check if we've already voted
// in this poll // in this poll
$this->__get('User');
if($this->User === null){ if($this->User === null){
$error->Add(new Exceptions\InvalidPatronException()); $error->Add(new Exceptions\InvalidPatronException());
} }

View file

@ -2,7 +2,7 @@
require_once('Core.php'); require_once('Core.php');
try{ try{
$urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true, '')), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/ $urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook $wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0 || !is_dir($wwwFilesystemPath)){ if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0 || !is_dir($wwwFilesystemPath)){

View file

@ -9,7 +9,7 @@ use function Safe\apcu_fetch;
use function Safe\shuffle; use function Safe\shuffle;
try{ try{
$urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true, '')), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/ $urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook $wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0){ if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0){

View file

@ -4,10 +4,10 @@ require_once('Core.php');
use function Safe\preg_replace; use function Safe\preg_replace;
try{ try{
$page = HttpInput::Int(GET, 'page', 1); $page = HttpInput::Int(GET, 'page') ?? 1;
$perPage = HttpInput::Int(GET, 'per-page', EBOOKS_PER_PAGE); $perPage = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE;
$query = HttpInput::Str(GET, 'query', false); $query = HttpInput::Str(GET, 'query', false) ?? '';
$tags = HttpInput::GetArray('tags', []); $tags = HttpInput::GetArray('tags') ?? [];
$collection = HttpInput::Str(GET, 'collection', false); $collection = HttpInput::Str(GET, 'collection', false);
$view = HttpInput::Str(GET, 'view', false); $view = HttpInput::Str(GET, 'view', false);
$sort = HttpInput::Str(GET, 'sort', false); $sort = HttpInput::Str(GET, 'sort', false);
@ -41,10 +41,6 @@ try{
$sort = null; $sort = null;
} }
if($query === ''){
$query = null;
}
if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){ if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){
$tags = []; $tags = [];
} }
@ -82,7 +78,7 @@ try{
} }
} }
else{ else{
$ebooks = Library::FilterEbooks($query, $tags, $sort); $ebooks = Library::FilterEbooks($query != '' ? $query : null, $tags, $sort);
$pageTitle = 'Browse Standard Ebooks'; $pageTitle = 'Browse Standard Ebooks';
$pageHeader = 'Browse Ebooks'; $pageHeader = 'Browse Ebooks';
$pages = ceil(sizeof($ebooks) / $perPage); $pages = ceil(sizeof($ebooks) / $perPage);
@ -154,7 +150,7 @@ catch(Exceptions\InvalidCollectionException $ex){
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks/?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a> <a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks/?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
</nav> </nav>
<? } ?> <? } ?>
<? if(sizeof($ebooks) > 0 && $query === null && sizeof($tags) == 0 && $collection === null && $page == 1){ ?> <? if(sizeof($ebooks) > 0 && $query == '' && sizeof($tags) == 0 && $collection === null && $page == 1){ ?>
<?= Template::ContributeAlert() ?> <?= Template::ContributeAlert() ?>
<? } ?> <? } ?>
</main> </main>

View file

@ -5,9 +5,9 @@ use Safe\DateTime;
$ebooks = []; $ebooks = [];
try{ try{
$query = HttpInput::Str(GET, 'query', false); $query = HttpInput::Str(GET, 'query', false) ?? '';
if($query !== null){ if($query !== ''){
$ebooks = Library::Search($query); $ebooks = Library::Search($query);
} }
} }

View file

@ -5,9 +5,9 @@ use Safe\DateTime;
$ebooks = []; $ebooks = [];
try{ try{
$query = HttpInput::Str(GET, 'query', false); $query = HttpInput::Str(GET, 'query', false) ?? '';
if($query !== null){ if($query !== ''){
$ebooks = Library::Search($query); $ebooks = Library::Search($query);
} }
} }

View file

@ -5,9 +5,9 @@ use Safe\DateTime;
$ebooks = []; $ebooks = [];
try{ try{
$query = HttpInput::Str(GET, 'query', false); $query = HttpInput::Str(GET, 'query', false) ?? '';
if($query !== null){ if($query !== ''){
$ebooks = Library::Search($query); $ebooks = Library::Search($query);
} }
} }

View file

@ -8,7 +8,7 @@ use function Safe\sort;
$currentManual = Manual::GetLatestVersion(); $currentManual = Manual::GetLatestVersion();
$url = HttpInput::Str(GET, 'url', true, ''); $url = HttpInput::Str(GET, 'url', true) ?? '';
$url = preg_replace('|^/|ius', '', $url); $url = preg_replace('|^/|ius', '', $url);
$url = preg_replace('|\.php$|ius', '', $url); $url = preg_replace('|\.php$|ius', '', $url);
$url = preg_replace('|/$|ius', '', $url); $url = preg_replace('|/$|ius', '', $url);

View file

@ -36,9 +36,9 @@ try{
$subscriber->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'newsletter', false); $subscriber->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'newsletter', false);
$subscriber->IsSubscribedToSummary = HttpInput::Bool(POST, 'monthlysummary', false); $subscriber->IsSubscribedToSummary = HttpInput::Bool(POST, 'monthlysummary', false);
$captcha = $_SESSION['captcha'] ?? null; $captcha = $_SESSION['captcha'] ?? '';
if($captcha === null || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false))){ if($captcha === '' || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false) ?? '')){
throw new Exceptions\ValidationException(new Exceptions\InvalidCaptchaException()); throw new Exceptions\ValidationException(new Exceptions\InvalidCaptchaException());
} }