Add poll system for Patrons Circle

This commit is contained in:
Alex Cabal 2022-06-29 16:51:45 -05:00
parent 3555d53615
commit 2ef5ce6551
44 changed files with 717 additions and 98 deletions

View file

@ -258,6 +258,16 @@ Define webroot /standardebooks.org/web
# Newsletter # Newsletter
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1 RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
# Polls
RewriteRule ^/patrons-circle/polls/([^/]+)$ /patrons-circle/polls/get.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/]+)/votes/(new|success)$ /patrons-circle/polls/votes/$2.php?pollurlname=$1
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1
</VirtualHost> </VirtualHost>
<VirtualHost *:80> <VirtualHost *:80>

View file

@ -257,4 +257,14 @@ Define webroot /standardebooks.org/web
# Newsletter # Newsletter
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1 RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
# Polls
RewriteRule ^/patrons-circle/polls/([^/]+)$ /patrons-circle/polls/get.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/]+)/votes/(new|success)$ /patrons-circle/polls/votes/$2.php?pollurlname=$1
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1
</VirtualHost> </VirtualHost>

View file

@ -19,6 +19,12 @@ parameters:
- '#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
- '#Constant EMAIL_SMTP_USERNAME not found.#'
level: level:
7 7
paths: paths:

View file

@ -0,0 +1,8 @@
CREATE TABLE `PollItems` (
`PollItemId` int(10) unsigned NOT NULL AUTO_INCREMENT,
`PollId` int(10) unsigned NOT NULL,
`Name` varchar(255) NOT NULL,
`Description` text DEFAULT NULL,
`SortOrder` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`PollItemId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

11
config/sql/se/Polls.sql Normal file
View file

@ -0,0 +1,11 @@
CREATE TABLE `Polls` (
`PollId` int(11) unsigned NOT NULL AUTO_INCREMENT,
`Created` datetime NOT NULL,
`Name` varchar(255) NOT NULL,
`UrlName` varchar(255) NOT NULL,
`Description` text DEFAULT NULL,
`Start` datetime DEFAULT NULL,
`End` datetime DEFAULT NULL,
PRIMARY KEY (`PollId`),
UNIQUE KEY `idxUnique` (`UrlName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6
config/sql/se/Votes.sql Normal file
View file

@ -0,0 +1,6 @@
CREATE TABLE `Votes` (
`UserId` int(11) unsigned NOT NULL,
`PollItemId` int(11) unsigned NOT NULL,
`Created` datetime NOT NULL,
UNIQUE KEY `idxUnique` (`PollItemId`,`UserId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -59,6 +59,13 @@ const HTTP_VAR_BOOL = 2;
const HTTP_VAR_DEC = 3; const HTTP_VAR_DEC = 3;
const HTTP_VAR_ARRAY = 4; const HTTP_VAR_ARRAY = 4;
const HTTP_GET = 0;
const HTTP_POST = 1;
const HTTP_PATCH = 2;
const HTTP_PUT = 3;
const HTTP_DELETE = 4;
const HTTP_HEAD = 5;
const VIEW_GRID = 'grid'; const VIEW_GRID = 'grid';
const VIEW_LIST = 'list'; const VIEW_LIST = 'list';

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class InvalidPatronException extends SeException{
protected $message = 'We couldnt locate you in the Patrons Circle.';
}

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class InvalidPollException extends SeException{
protected $message = 'We couldnt locate that poll.';
}

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class InvalidPollItemException extends SeException{
protected $message = 'We couldnt locate that poll item.';
}

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class InvalidUserException extends SeException{
protected $message = 'We couldnt locate you in our system.';
}

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class PollClosedException extends SeException{
protected $message = 'This poll is not open to voting right now.';
}

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class PollItemRequiredException extends SeException{
protected $message = 'You must select an item to vote on.';
}

View file

@ -8,6 +8,12 @@ class ValidationException extends SeException{
public $HasExceptions = false; public $HasExceptions = false;
public $IsFatal = false; public $IsFatal = false;
public function __construct(?\Exception $exception = null){
if($exception !== null){
$this->Add($exception);
}
}
public function __toString(): string{ public function __toString(): string{
$output = ''; $output = '';
foreach($this->Exceptions as $exception){ foreach($this->Exceptions as $exception){

View file

@ -0,0 +1,6 @@
<?
namespace Exceptions;
class VoteExistsException extends SeException{
protected $message = 'Youve already voted in this poll.';
}

View file

@ -1,4 +1,5 @@
<? <?
use Safe\DateTime;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\file_put_contents; use function Safe\file_put_contents;
use function Safe\tempnam; use function Safe\tempnam;

View file

@ -1,5 +1,30 @@
<? <?
use function Safe\preg_match;
class HttpInput{ class HttpInput{
public static function RequestMethod(): int{
$method = $_POST['_method'] ?? $_SERVER['REQUEST_METHOD'];
switch($method){
case 'POST':
return HTTP_POST;
case 'PUT':
return HTTP_PUT;
case 'DELETE':
return HTTP_DELETE;
case 'PATCH':
return HTTP_PATCH;
case 'HEAD':
return HTTP_HEAD;
}
return HTTP_GET;
}
public static function RequestType(): int{
return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST;
}
public static function Str(string $type, string $variable, bool $allowEmptyString = true, string $default = null): ?string{ public static function Str(string $type, string $variable, bool $allowEmptyString = true, string $default = null): ?string{
$var = self::GetHttpVar($variable, HTTP_VAR_STR, $type, $default); $var = self::GetHttpVar($variable, HTTP_VAR_STR, $type, $default);

View file

@ -1,4 +1,5 @@
<? <?
use Safe\DateTime;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
class NewsletterSubscriber extends PropertiesBase{ class NewsletterSubscriber extends PropertiesBase{
@ -26,9 +27,10 @@ class NewsletterSubscriber extends PropertiesBase{
$uuid = Uuid::uuid4(); $uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString(); $this->Uuid = $uuid->toString();
$this->Timestamp = new DateTime();
try{ try{
Db::Query('insert into NewsletterSubscribers (Email, Uuid, FirstName, LastName, IsConfirmed, IsSubscribedToNewsletter, IsSubscribedToSummary, Timestamp) values (?, ?, ?, ?, ?, ?, ?, utc_timestamp());', [$this->Email, $this->Uuid, $this->FirstName, $this->LastName, false, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary]); Db::Query('INSERT into NewsletterSubscribers (Email, Uuid, FirstName, LastName, IsConfirmed, IsSubscribedToNewsletter, IsSubscribedToSummary, Timestamp) values (?, ?, ?, ?, ?, ?, ?, ?);', [$this->Email, $this->Uuid, $this->FirstName, $this->LastName, false, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary, $this->Timestamp]);
} }
catch(PDOException $ex){ catch(PDOException $ex){
if($ex->errorInfo[1] == 1062){ if($ex->errorInfo[1] == 1062){
@ -53,11 +55,11 @@ class NewsletterSubscriber extends PropertiesBase{
} }
public function Confirm(): void{ public function Confirm(): void{
Db::Query('update NewsletterSubscribers set IsConfirmed = true where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]); Db::Query('UPDATE NewsletterSubscribers set IsConfirmed = true where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
} }
public function Delete(): void{ public function Delete(): void{
Db::Query('delete from NewsletterSubscribers where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]); Db::Query('DELETE from NewsletterSubscribers where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
} }
public function Validate(): void{ public function Validate(): void{
@ -77,11 +79,7 @@ class NewsletterSubscriber extends PropertiesBase{
} }
public static function Get(string $uuid): NewsletterSubscriber{ public static function Get(string $uuid): NewsletterSubscriber{
if($uuid == ''){ $subscribers = Db::Query('SELECT * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
throw new Exceptions\InvalidNewsletterSubscriberException();
}
$subscribers = Db::Query('select * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
if(sizeof($subscribers) == 0){ if(sizeof($subscribers) == 0){
throw new Exceptions\InvalidNewsletterSubscriberException(); throw new Exceptions\InvalidNewsletterSubscriberException();

View file

@ -1,4 +1,5 @@
<? <?
use Safe\DateTime;
use function Safe\file_put_contents; use function Safe\file_put_contents;
class OpdsFeed extends AtomFeed{ class OpdsFeed extends AtomFeed{

View file

@ -1,27 +0,0 @@
<?
use Safe\DateTime;
use function Safe\substr;
abstract class OrmBase{
final public function __construct(){
// Satisfy PHPStan and prevent child classes from having their own constructor
}
public static function FillObject(Object $object, array $row): Object{
foreach($row as $property => $value){
if(substr($property, strlen($property) - 5) == 'Cache'){
$property = substr($property, 0, strlen($property) - 5);
$object->$property = $value;
}
else{
$object->$property = $value;
}
}
return $object;
}
public static function FromRow(array $row): Object{
return self::FillObject(new static(), $row);
}
}

View file

@ -10,22 +10,30 @@ class Patron extends PropertiesBase{
public $Timestamp = null; public $Timestamp = null;
public $DeactivatedTimestamp = null; public $DeactivatedTimestamp = null;
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');
return $result[0] ?? null; if(sizeof($result) == 0){
} throw new Exceptions\InvalidPatronException();
protected function GetUser(): ?User{
if($this->User === null && $this->UserId !== null){
$this->User = User::Get($this->UserId);
} }
return $this->User; return $result[0];
}
public static function GetByEmail(?string $email): Patron{
$result = Db::Query('SELECT p.* from Patrons p inner join Users u on p.UserId = u.UserId where u.Email = ?', [$email], 'Patron');
if(sizeof($result) == 0){
throw new Exceptions\InvalidPatronException();
}
return $result[0];
} }
public function Create(bool $sendEmail = true): void{ public function Create(bool $sendEmail = true): void{
Db::Query('insert into Patrons (Timestamp, UserId, IsAnonymous, AlternateName, IsSubscribedToEmail) values(utc_timestamp(), ?, ?, ?, ?);', [$this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmail]); $this->Timestamp = new DateTime();
Db::Query('INSERT into Patrons (Timestamp, UserId, IsAnonymous, AlternateName, IsSubscribedToEmail) values(?, ?, ?, ?, ?);', [$this->Timestamp, $this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmail]);
if($sendEmail){ if($sendEmail){
$this->SendWelcomeEmail(); $this->SendWelcomeEmail();
@ -33,7 +41,7 @@ class Patron extends PropertiesBase{
} }
public function Reactivate(bool $sendEmail = true): void{ public function Reactivate(bool $sendEmail = true): void{
Db::Query('update Patrons set Timestamp = utc_timestamp(), DeactivatedTimestamp = null, IsAnonymous = ?, IsSubscribedToEmail = ?, AlternateName = ? where UserId = ?;', [$this->IsAnonymous, $this->IsSubscribedToEmail, $this->AlternateName, $this->UserId]); Db::Query('UPDATE Patrons set Timestamp = utc_timestamp(), DeactivatedTimestamp = null, IsAnonymous = ?, IsSubscribedToEmail = ?, AlternateName = ? where UserId = ?;', [$this->IsAnonymous, $this->IsSubscribedToEmail, $this->AlternateName, $this->UserId]);
$this->Timestamp = new DateTime(); $this->Timestamp = new DateTime();
$this->DeactivatedTimestamp = null; $this->DeactivatedTimestamp = null;
@ -43,7 +51,7 @@ class Patron extends PropertiesBase{
} }
private function SendWelcomeEmail(): void{ private function SendWelcomeEmail(): void{
$this->GetUser(); $this->__get('User');
if($this->User !== null){ if($this->User !== null){
$em = new Email(); $em = new Email();
$em->To = $this->User->Email; $em->To = $this->User->Email;

View file

@ -11,21 +11,13 @@ class Payment extends PropertiesBase{
public $Fee; public $Fee;
public $IsRecurring; public $IsRecurring;
protected function GetUser(): ?User{
if($this->User === null && $this->UserId !== null){
$this->User = User::Get($this->UserId);
}
return $this->User;
}
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
// If the User object isn't null, then check if we already have this user in our system // If the User object isn't null, then check if we already have this user in our system
if($this->User !== null && $this->User->Email !== null){ if($this->User !== null && $this->User->Email !== null){
$result = Db::Query('select * from Users where Email = ?', [$this->User->Email], 'User'); $result = Db::Query('SELECT * from Users where Email = ?', [$this->User->Email], 'User');
if(sizeof($result) == 0){ if(sizeof($result) == 0){
// User doesn't exist, create it now // User doesn't exist, create it now
@ -41,7 +33,7 @@ class Payment extends PropertiesBase{
} }
try{ try{
Db::Query('insert into Payments (UserId, Timestamp, ChannelId, TransactionId, Amount, Fee, IsRecurring) values(?, ?, ?, ?, ?, ?, ?);', [$this->UserId, $this->Timestamp, $this->ChannelId, $this->TransactionId, $this->Amount, $this->Fee, $this->IsRecurring]); Db::Query('INSERT into Payments (UserId, Timestamp, ChannelId, TransactionId, Amount, Fee, IsRecurring) values(?, ?, ?, ?, ?, ?, ?);', [$this->UserId, $this->Timestamp, $this->ChannelId, $this->TransactionId, $this->Amount, $this->Fee, $this->IsRecurring]);
} }
catch(PDOException $ex){ catch(PDOException $ex){
if($ex->errorInfo[1] == 1062){ if($ex->errorInfo[1] == 1062){

88
lib/Poll.php Normal file
View file

@ -0,0 +1,88 @@
<?
use Safe\DateTime;
use function Safe\usort;
class Poll extends PropertiesBase{
public $PollId;
public $Name;
public $UrlName;
public $Description;
public $Created;
public $Start;
public $End;
protected $Url = null;
protected $PollItems = null;
protected $PollItemsByWinner = null;
protected $VoteCount = null;
protected function GetUrl(): string{
if($this->Url === null){
$this->Url = '/patrons-circle/polls/' . $this->UrlName;
}
return $this->Url;
}
protected function GetVoteCount(): int{
if($this->VoteCount === null){
$this->VoteCount = (Db::Query('select count(*) as VoteCount from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollId = ?', [$this->PollId]))[0]->VoteCount;
}
return $this->VoteCount;
}
/**
* @return array<PollItem>
*/
protected function GetPollItems(): array{
if($this->PollItems === null){
$this->PollItems = Db::Query('SELECT * from PollItems where PollId = ? order by SortOrder asc', [$this->PollId], 'PollItem');
}
return $this->PollItems;
}
/**
* @return array<PollItem>
*/
protected function GetPollItemsByWinner(): array{
if($this->PollItemsByWinner === null){
$this->__get('PollItems');
$this->PollItemsByWinner = $this->PollItems;
usort($this->PollItemsByWinner, function(PollItem $a, PollItem $b){ return $a->VoteCount <=> $b->VoteCount; });
$this->PollItemsByWinner = array_reverse($this->PollItemsByWinner);
}
return $this->PollItemsByWinner;
}
public function IsActive(): bool{
$now = new DateTime();
if( ($this->Start !== null && $this->Start > $now) || ($this->End !== null && $this->End < $now)){
return false;
}
return true;
}
public static function Get(?int $pollId): Poll{
$result = Db::Query('SELECT * from Polls where PollId = ?', [$pollId], 'Poll');
if(sizeof($result) == 0){
throw new Exceptions\InvalidPollException();
}
return $result[0];
}
public static function GetByUrlName(?string $urlName): Poll{
$result = Db::Query('SELECT * from Polls where UrlName = ?', [$urlName], 'Poll');
if(sizeof($result) == 0){
throw new Exceptions\InvalidPollException();
}
return $result[0];
}
}

27
lib/PollItem.php Normal file
View file

@ -0,0 +1,27 @@
<?
class PollItem extends PropertiesBase{
public $PollItemId;
public $PollId;
public $Name;
public $Description;
protected $VoteCount = null;
protected $Poll = null;
protected function GetVoteCount(): int{
if($this->VoteCount === null){
$this->VoteCount = (Db::Query('select count(*) as VoteCount from Votes v inner join PollItems pi on v.PollItemId = pi.PollItemId where pi.PollItemId = ?', [$this->PollItemId]))[0]->VoteCount;
}
return $this->VoteCount;
}
public static function Get(?int $pollItemId): PollItem{
$result = Db::Query('SELECT * from PollItems where PollItemId = ?', [$pollItemId], 'PollItem');
if(sizeof($result) == 0){
throw new Exceptions\InvalidPollItemException();
}
return $result[0];
}
}

View file

@ -1,7 +1,7 @@
<? <?
use function Safe\substr; use function Safe\substr;
abstract class PropertiesBase extends OrmBase{ abstract class PropertiesBase{
/** /**
* @param mixed $var * @param mixed $var
* @return mixed * @return mixed
@ -12,6 +12,15 @@ abstract class PropertiesBase extends OrmBase{
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')){
// If our object has an VarId attribute, and the Var class also has a ::Get method,
// call it and return the result
if($this->$var === null && $this->{$var . 'Id'} !== null){
$this->$var = $var::Get($this->{$var . 'Id'});
}
return $this->$var;
}
elseif(substr($var, 0, 7) == 'Display'){ elseif(substr($var, 0, 7) == 'Display'){
// If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML. // If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML.
if($this->$var === null){ if($this->$var === null){

View file

@ -1,4 +1,9 @@
<? <?
use Safe\DateTime;
use function Safe\file_get_contents;
use function Safe\filesize;
use function Safe\preg_replace;
class RssFeed extends Feed{ class RssFeed extends Feed{
public $Description; public $Description;

View file

@ -1,5 +1,6 @@
<? <?
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Safe\DateTime;
class User extends PropertiesBase{ class User extends PropertiesBase{
public $UserId; public $UserId;
@ -14,10 +15,24 @@ class User extends PropertiesBase{
public $Timestamp; public $Timestamp;
public $Uuid; public $Uuid;
public static function Get(int $userId): ?User{ public static function Get(?int $userId): User{
$result = Db::Query('select * from Users where UserId = ?', [$userId], 'User'); $result = Db::Query('SELECT * from Users where UserId = ?', [$userId], 'User');
return $result[0] ?? null; 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];
} }
protected function GetName(): string{ protected function GetName(): string{
@ -31,9 +46,10 @@ class User extends PropertiesBase{
public function Create(): void{ public function Create(): void{
$uuid = Uuid::uuid4(); $uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString(); $this->Uuid = $uuid->toString();
$this->Timestamp = new DateTime();
try{ try{
Db::Query('insert into Users (Email, Name, Uuid, Timestamp) values (?, ?, ?, utc_timestamp());', [$this->Email, $this->Name, $this->Uuid]); Db::Query('INSERT into Users (Email, Name, Uuid, Timestamp) values (?, ?, ?, ?);', [$this->Email, $this->Name, $this->Uuid, $this->Timestamp]);
} }
catch(PDOException $ex){ catch(PDOException $ex){
if($ex->errorInfo[1] == 1062){ if($ex->errorInfo[1] == 1062){

87
lib/Vote.php Normal file
View file

@ -0,0 +1,87 @@
<?
use Safe\DateTime;
class Vote extends PropertiesBase{
public $VoteId;
public $UserId;
protected $User = null;
public $Created;
public $PollItemId;
protected $PollItem = null;
protected $Url = null;
protected function GetUrl(): string{
if($this->Url === null){
$this->Url = '/patrons-circle/polls/' . $this->PollItem->Poll->Url . '/votes/' . $this->UserId;
}
return $this->Url;
}
protected function Validate(): void{
$error = new Exceptions\ValidationException();
if($this->UserId === null){
$error->Add(new Exceptions\InvalidPatronException());
}
if($this->PollItemId === null){
$error->Add(new Exceptions\PollItemRequiredException());
}
else{
$this->__get('PollItem');
if($this->PollItem === null){
$error->Add(new Exceptions\InvalidPollException());
}
}
if(!$this->PollItem->Poll->IsActive()){
$error->Add(new Exceptions\PollClosedException());
}
if(!$error->HasExceptions){
// Basic sanity checks done, now check if we've already voted
// in this poll
$this->__get('User');
if($this->User === null){
$error->Add(new Exceptions\InvalidPatronException());
}
else{
// Do we already have a vote for this poll, from this user?
if( (Db::Query('
SELECT count(*) as VoteCount from Votes v inner join
(select PollItemId from PollItems pi inner join Polls p on pi.PollId = p.PollId) x
on v.PollItemId = x.PollItemId where v.UserId = ?', [$this->UserId]))[0]->VoteCount > 0){
$error->Add(new Exceptions\VoteExistsException());
}
}
}
if($error->HasExceptions){
throw $error;
}
}
public function Create(?string $email = null): void{
if($email !== null){
try{
$patron = Patron::GetByEmail($email);
$this->UserId = $patron->UserId;
$this->User = $patron->User;
}
catch(Exceptions\InvalidPatronException $ex){
// 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;
}
}
$this->Validate();
$this->Created = new DateTime();
Db::Query('INSERT into Votes (UserId, PollItemId, Created) values (?, ?, ?)', [$this->UserId, $this->PollItemId, $this->Created]);
}
}

View file

@ -3,5 +3,5 @@
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
// Delete unconfirmed newsletter subscribers who are more than a week old // Delete unconfirmed newsletter subscribers who are more than a week old
Db::Query('delete from NewsletterSubscribers where IsConfirmed = false and datediff(utc_timestamp(), Timestamp) >= 7'); Db::Query('DELETE from NewsletterSubscribers where IsConfirmed = false and datediff(utc_timestamp(), Timestamp) >= 7');
?> ?>

View file

@ -160,9 +160,10 @@ try{
if(($payment->IsRecurring && $payment->Amount >= 10 && $payment->Timestamp >= $lastMonth) || ($payment->Amount >= 100 && $payment->Timestamp >= $lastYear)){ if(($payment->IsRecurring && $payment->Amount >= 10 && $payment->Timestamp >= $lastMonth) || ($payment->Amount >= 100 && $payment->Timestamp >= $lastYear)){
// This payment is eligible for the Patrons Circle. // This payment is eligible for the Patrons Circle.
// Are we already a patron? // Are we already a patron?
$patron = Patron::Get($payment->UserId); try{
$patron = Patron::Get($payment->UserId);
if($patron === null){ }
catch(Exceptions\InvalidPatronException $ex){
// Not a patron yet, add them to the Patrons Circle // Not a patron yet, add them to the Patrons Circle
$patron = new Patron(); $patron = new Patron();
$patron->UserId = $payment->UserId; $patron->UserId = $payment->UserId;
@ -194,7 +195,7 @@ try{
else{ else{
// Not a patron; send a thank you email anyway, but only if this is a non-recurring donation, // Not a patron; send a thank you email anyway, but only if this is a non-recurring donation,
// or if it's their very first recurring donation // or if it's their very first recurring donation
$previousPaymentCount = (Db::Query('select count(*) as PreviousPaymentCount from Payments where UserId = ? and IsRecurring = true', [$payment->UserId]))[0]->PreviousPaymentCount; $previousPaymentCount = (Db::Query('SELECT count(*) as PreviousPaymentCount from Payments where UserId = ? and IsRecurring = true', [$payment->UserId]))[0]->PreviousPaymentCount;
// We just added a payment to the system, so if this is their very first recurring payment, we expect the count to be exactly 1 // We just added a payment to the system, so if this is their very first recurring payment, we expect the count to be exactly 1
if(!$payment->IsRecurring || $previousPaymentCount == 1){ if(!$payment->IsRecurring || $previousPaymentCount == 1){
@ -210,7 +211,7 @@ try{
} }
} }
Db::Query('delete from PendingPayments where TransactionId = ?;', [$pendingPayment->TransactionId]); Db::Query('DELETE from PendingPayments where TransactionId = ?;', [$pendingPayment->TransactionId]);
$log->Write('Donation processed.'); $log->Write('Donation processed.');
} }

View file

@ -9,7 +9,7 @@ $startDate = new DateTime('2022-07-01');
$endDate = new DateTime('2022-07-31'); $endDate = new DateTime('2022-07-31');
$autoHide = $autoHide ?? true; $autoHide = $autoHide ?? true;
$showDonateButton = $showDonateButton ?? true; $showDonateButton = $showDonateButton ?? true;
$current = (Db::Query('select count(*) as PatronCount from Patrons where Timestamp >= ?', [$startDate]))[0]->PatronCount; $current = (Db::Query('SELECT count(*) as PatronCount from Patrons where Timestamp >= ?', [$startDate]))[0]->PatronCount;
$target = 70; $target = 70;
$stretchCurrent = 0; $stretchCurrent = 0;
$stretchTarget = 20; $stretchTarget = 20;

View file

@ -7,7 +7,7 @@ $anonymousPatronCount = 0;
// Get the Patrons Circle and try to sort by last name ascending // Get the Patrons Circle and try to sort by last name ascending
// See <https://mariadb.com/kb/en/pcre/#unicode-character-properties> for Unicode character properties // See <https://mariadb.com/kb/en/pcre/#unicode-character-properties> for Unicode character properties
$patronsCircle = Db::Query('select if(p.AlternateName is not null, p.AlternateName, u.Name) as SortedName $patronsCircle = Db::Query('SELECT if(p.AlternateName is not null, p.AlternateName, u.Name) as SortedName
from Patrons p inner join Users u from Patrons p inner join Users u
on p.UserId = u.UserId on p.UserId = u.UserId
where where
@ -16,7 +16,7 @@ $patronsCircle = Db::Query('select if(p.AlternateName is not null, p.AlternateNa
order by regexp_substr(SortedName, "[\\\p{Lu}][\\\p{L}\-]+$") asc; order by regexp_substr(SortedName, "[\\\p{Lu}][\\\p{L}\-]+$") asc;
'); ');
$anonymousPatronCount = Db::Query('select sum(cnt) as AnonymousPatronCount $anonymousPatronCount = Db::Query('SELECT sum(cnt) as AnonymousPatronCount
from from
( (
( (

View file

@ -1836,6 +1836,16 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.button-row.narrow{
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.button-row:last-child{
margin-bottom: 0;
}
.masthead h2 + section > h3{ .masthead h2 + section > h3{
margin-top: 0; margin-top: 0;
} }
@ -2047,14 +2057,16 @@ article.ebook h2 + section > h3:first-of-type{
left: -5000px; left: -5000px;
} }
form[action*="/polls/"],
form[action="/newsletter/subscribers"]{ form[action="/newsletter/subscribers"]{
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 2rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 0; margin-bottom: 0;
} }
form[action*="/polls/"] label.email,
form[action="/newsletter/subscribers"] label.email, form[action="/newsletter/subscribers"] label.email,
form[action="/newsletter/subscribers"] label.captcha{ form[action="/newsletter/subscribers"] label.captcha{
grid-column: 1 / span 2; grid-column: 1 / span 2;
@ -2070,18 +2082,19 @@ form[action="/newsletter/subscribers"] label.captcha div input{
align-self: center; align-self: center;
} }
form[action="/newsletter/subscribers"] ul{ form fieldset ul{
list-style: none; list-style: none;
} }
form[action*="/polls/"] button,
form[action="/newsletter/subscribers"] button{ form[action="/newsletter/subscribers"] button{
grid-column: 2; grid-column: 2;
justify-self: end; justify-self: end;
margin-left: 0; margin-left: 0;
} }
form[action*="/polls/"] fieldset,
form[action="/newsletter/subscribers"] fieldset{ form[action="/newsletter/subscribers"] fieldset{
margin-top: 1rem;
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }
@ -2096,15 +2109,25 @@ fieldset p{
label.checkbox{ label.checkbox{
display: inline-flex; display: inline-flex;
align-items: center; align-items: flex-start;
text-align: left; text-align: left;
line-height: 1; line-height: 1;
cursor: pointer;
} }
label.checkbox input{ label.checkbox input{
margin-right: .25rem; margin-right: .25rem;
} }
label.checkbox span{
display: block;
}
label.checkbox span > span{
line-height: 1.6;
margin-top: .25rem;
}
article.step-by-step-guide ol ol{ article.step-by-step-guide ol ol{
margin-left: 1.2rem; margin-left: 1.2rem;
list-style: decimal; list-style: decimal;
@ -2130,19 +2153,12 @@ aside header{
font-size: 1.5rem; font-size: 1.5rem;
} }
.meter,
.progress{ .progress{
position: relative; position: relative;
font-size: 0; font-size: 0;
} }
.progress > div{
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.donation a.button{ .donation a.button{
display: inline-block; display: inline-block;
white-space: normal; white-space: normal;
@ -2215,6 +2231,7 @@ aside header{
hyphens: auto; hyphens: auto;
} }
.meter p,
.progress p{ .progress p{
font-size: 1rem; font-size: 1rem;
font-family: "League Spartan", Arial, sans-serif; font-family: "League Spartan", Arial, sans-serif;
@ -2251,7 +2268,14 @@ aside header{
font-size: .75rem; font-size: .75rem;
} }
.meter > div,
.progress > div{ .progress > div{
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
/* Animate the div instead of the bar itself, because animating the bar triggers an /* Animate the div instead of the bar itself, because animating the bar triggers an
FF bug that causes infinite requsts to stripes.svg */ FF bug that causes infinite requsts to stripes.svg */
background: url("/images/stripes.svg") transparent; background: url("/images/stripes.svg") transparent;
@ -2260,6 +2284,7 @@ aside header{
z-index: 3; z-index: 3;
} }
meter,
progress{ progress{
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
@ -2280,6 +2305,7 @@ progress::-webkit-progress-value{
box-shadow: 1px 0 1px rgba(0, 0, 0, .25); box-shadow: 1px 0 1px rgba(0, 0, 0, .25);
} }
meter::-moz-meter-bar,
progress::-moz-progress-bar{ progress::-moz-progress-bar{
background: var(--button); background: var(--button);
box-shadow: 1px 0 1px rgba(0, 0, 0, .25); box-shadow: 1px 0 1px rgba(0, 0, 0, .25);
@ -2507,6 +2533,33 @@ ul.feed p{
word-break: break-word; word-break: break-word;
} }
.center-notice{
text-align: center;
font-style: italic;
}
.votes{
margin-top: 2rem;
}
.votes td:first-child{
font-weight: bold;
}
.votes td{
width: 50%;
padding: 1rem;
text-align: left;
}
.votes tr:first-child td{
padding-top: 0;
}
.votes tr:last-child td{
padding-bottom: 0;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */ @media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */ /* For iPad, unset the height so it matches the other elements */
select[multiple]{ select[multiple]{
@ -3016,6 +3069,17 @@ ul.feed p{
form[action="/settings"] select{ form[action="/settings"] select{
width: 100%; width: 100%;
} }
.votes tr,
.votes tr td{
display: block;
width: 100%;
padding: 0;
}
.votes tr + tr{
margin-top: 2rem;
}
} }
@media(max-width: 470px){ @media(max-width: 470px){

View file

@ -3,11 +3,11 @@ require_once('Core.php');
use function Safe\preg_match; use function Safe\preg_match;
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST; $requestType = HttpInput::RequestType();
try{ try{
// We may use GET if we're called from an unsubscribe link in an email // We may use GET if we're called from an unsubscribe link in an email
if(!in_array($_SERVER['REQUEST_METHOD'], ['DELETE', 'GET'])){ if(!in_array(HttpInput::RequestMethod(), [HTTP_DELETE, HTTP_GET])){
throw new Exceptions\InvalidRequestException(); throw new Exceptions\InvalidRequestException();
} }

View file

@ -4,14 +4,14 @@ require_once('Core.php');
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\session_unset; use function Safe\session_unset;
if($_SERVER['REQUEST_METHOD'] != 'POST'){ if(HttpInput::RequestMethod() != HTTP_POST){
http_response_code(405); http_response_code(405);
exit(); exit();
} }
session_start(); session_start();
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST; $requestType = HttpInput::RequestType();
$subscriber = new NewsletterSubscriber(); $subscriber = new NewsletterSubscriber();
@ -39,11 +39,7 @@ try{
$captcha = $_SESSION['captcha'] ?? null; $captcha = $_SESSION['captcha'] ?? null;
if($captcha === null || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false))){ if($captcha === null || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false))){
$error = new Exceptions\ValidationException(); throw new Exceptions\ValidationException(new Exceptions\InvalidCaptchaException());
$error->Add(new Exceptions\InvalidCaptchaException());
throw $error;
} }
$subscriber->Create(); $subscriber->Create();

View file

@ -0,0 +1,3 @@
<?
header('Location: /donate#patrons-circle');
exit();

View file

@ -0,0 +1,40 @@
<?
require_once('Core.php');
use Safe\DateTime;
$poll = null;
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
?><?= Template::Header(['title' => $poll->Name, 'highlight' => '', 'description' => $poll->Description]) ?>
<main>
<article>
<h1><?= Formatter::ToPlainText($poll->Name) ?></h1>
<p><?= $poll->Description ?></p>
<? if($poll->IsActive()){ ?>
<? if($poll->End !== null){ ?>
<p class="center-notice">This poll closes on <?= $poll->End->format('F j, Y g:i A') ?>.</p>
<? } ?>
<p class="button-row narrow">
<a href="<?= $poll->Url ?>/votes/new" class="button">Vote now</a>
<a href="<?= $poll->Url ?>/votes" class="button">View results</a>
</p>
<? }else{ ?>
<? if($poll->Start !== null && $poll->Start > new DateTime()){ ?>
<p class="center-notice">This poll opens on <?= $poll->Start->format('F j, Y g:i A') ?>.</p>
<? }else{ ?>
<p class="center-notice">This poll closed on <?= $poll->End->format('F j, Y g:i A') ?>.</p>
<p class="button-row narrow"><a href="<?= $poll->Url ?>/votes" class="button">View results</a></p>
<? } ?>
<? } ?>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,46 @@
<?
require_once('Core.php');
$poll = null;
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
?><?= Template::Header(['title' => 'Results for the ' . $poll->Name . ' poll', 'highlight' => '', 'description' => 'The voting results for the ' . $poll->Name . ' poll.']) ?>
<main>
<article>
<h1>Results for the <?= Formatter::ToPlainText($poll->Name) ?> Poll</h1>
<p class="center-notice">Total votes: <?= number_format($poll->VoteCount) ?></p>
<? if($poll->IsActive()){ ?>
<? if($poll->End !== null){ ?>
<p class="center-notice">This poll closes on <?= $poll->End->format('F j, Y g:i A') ?>.</p>
<? } ?>
<? }elseif($poll->End !== null){ ?>
<p class="center-notice">This poll closed on <?= $poll->End->format('F j, Y g:i A') ?>.</p>
<? } ?>
<table class="votes">
<tbody>
<? foreach($poll->PollItemsByWinner as $pollItem){ ?>
<tr>
<td><?= Formatter::ToPlainText($pollItem->Name) ?></td>
<td>
<div class="meter">
<div aria-hidden="true">
<p><?= number_format($pollItem->VoteCount) ?></p>
</div>
<meter min="0" max="<?= $poll->VoteCount ?>" value="<?= $pollItem->VoteCount ?>"></meter>
</div>
</td>
</tr>
<? } ?>
</tbody>
</table>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,58 @@
<?
require_once('Core.php');
use function Safe\session_unset;
session_start();
$vote = $_SESSION['vote'] ?? new Vote();
$exception = $_SESSION['exception'] ?? null;
$poll = null;
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
if($exception){
http_response_code(400);
session_unset();
}
?><?= Template::Header(['title' => $poll->Name . ' - Vote Now', 'highlight' => '', 'description' => 'Vote in the ' . $poll->Name . ' poll']) ?>
<main>
<article>
<h1>Vote in the <?= Formatter::ToPlainText($poll->Name) ?> Poll</h1>
<?= Template::Error(['exception' => $exception]) ?>
<form method="post" action="<?= Formatter::ToPlainText($poll->Url) ?>/votes">
<label class="email">Your email address
<input type="email" name="email" value="<? if($vote->User !== null){ ?><?= Formatter::ToPlainText($vote->User->Email) ?><? } ?>" maxlength="80" required="required" />
</label>
<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"<? } ?>/>
<span>
<b><?= Formatter::ToPlainText($pollItem->Name) ?></b>
<? if($pollItem->Description !== null){ ?>
<span><?= Formatter::ToPlainText($pollItem->Description) ?></span>
<? } ?>
</span>
</label>
</li>
<? } ?>
</ul>
</fieldset>
<button>Vote</button>
</form>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,51 @@
<?
require_once('Core.php');
use function Safe\preg_match;
use function Safe\session_unset;
if(HttpInput::RequestMethod() != HTTP_POST){
http_response_code(405);
exit();
}
session_start();
$requestType = HttpInput::RequestType();
$vote = new Vote();
try{
$error = new Exceptions\ValidationException();
$vote->PollItemId = HttpInput::Int(POST, 'pollitemid');
$vote->Create(HttpInput::Str(POST, 'email', false));
session_unset();
if($requestType == WEB){
http_response_code(303);
header('Location: ' . $vote->PollItem->Poll->Url . '/votes/success');
}
else{
// Access via REST api; 201 CREATED with location
http_response_code(201);
header('Location: ' . $vote->Url);
}
}
catch(Exceptions\SeException $ex){
// Validation failed
if($requestType == WEB){
$_SESSION['vote'] = $vote;
$_SESSION['exception'] = $ex;
// Access via form; 303 redirect to the form, which will emit a 400 BAD REQUEST
http_response_code(303);
header('Location: /patrons-circle/polls/' . HttpInput::Str(GET, 'pollurlname', false) . '/votes/new');
}
else{
// Access via REST api; 400 BAD REQUEST
http_response_code(400);
}
}

View file

@ -0,0 +1,23 @@
<?
require_once('Core.php');
$poll = null;
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
?><?= Template::Header(['title' => 'Thank you for voting!', 'highlight' => 'newsletter', 'description' => 'Thank you for voting in a Standard Ebooks poll!']) ?>
<main>
<article>
<h1>Thank you for voting!</h1>
<p class="center-notice">Your vote in the <?= Formatter::ToPlainText($poll->Name) ?> poll has been recorded.</p>
<p class="button-row narrow"><a class="button" href="<?= $poll->Url ?>/votes"> view results</a></p>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -17,7 +17,7 @@ $lastPushHashFlag = '';
try{ try{
$log->Write('Received GitHub webhook.'); $log->Write('Received GitHub webhook.');
if($_SERVER['REQUEST_METHOD'] != 'POST'){ if(HttpInput::RequestMethod() != HTTP_POST){
throw new Exceptions\WebhookException('Expected HTTP POST.'); throw new Exceptions\WebhookException('Expected HTTP POST.');
} }

View file

@ -15,7 +15,7 @@ try{
$log->Write('Received Postmark webhook.'); $log->Write('Received Postmark webhook.');
if($_SERVER['REQUEST_METHOD'] != 'POST'){ if(HttpInput::RequestMethod() != HTTP_POST){
throw new Exceptions\WebhookException('Expected HTTP POST.'); throw new Exceptions\WebhookException('Expected HTTP POST.');
} }
@ -36,7 +36,7 @@ try{
// Received when a user marks an email as spam // Received when a user marks an email as spam
$log->Write('Event type: spam complaint.'); $log->Write('Event type: spam complaint.');
Db::Query('delete from NewsletterSubscribers where Email = ?', [$post->Email]); Db::Query('DELETE from NewsletterSubscribers where Email = ?', [$post->Email]);
} }
elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressSending){ elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressSending){
// Received when a user clicks Postmark's "Unsubscribe" link in a newsletter email // Received when a user clicks Postmark's "Unsubscribe" link in a newsletter email
@ -45,7 +45,7 @@ try{
$email = $post->Recipient; $email = $post->Recipient;
// Remove the email from our newsletter list // Remove the email from our newsletter list
Db::Query('delete from NewsletterSubscribers where Email = ?', [$email]); Db::Query('DELETE from NewsletterSubscribers where Email = ?', [$email]);
// Remove the suppression from Postmark, since we deleted it from our own list we will never email them again anyway // Remove the suppression from Postmark, since we deleted it from our own list we will never email them again anyway
$handle = curl_init(); $handle = curl_init();

View file

@ -1,6 +1,7 @@
<? <?
require_once('Core.php'); require_once('Core.php');
use Safe\DateTime;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\preg_replace; use function Safe\preg_replace;
@ -15,7 +16,7 @@ $log = new Log(ZOHO_WEBHOOK_LOG_FILE_PATH);
try{ try{
$log->Write('Received Zoho webhook.'); $log->Write('Received Zoho webhook.');
if($_SERVER['REQUEST_METHOD'] != 'POST'){ if(HttpInput::RequestMethod() != HTTP_POST){
throw new Exceptions\WebhookException('Expected HTTP POST.'); throw new Exceptions\WebhookException('Expected HTTP POST.');
} }
@ -53,7 +54,7 @@ try{
$payment->Create(); $payment->Create();
} }
else{ else{
Db::Query('insert into PendingPayments (Timestamp, ChannelId, TransactionId) values (utc_timestamp(), ?, ?);', [PAYMENT_CHANNEL_FA, $transactionId]); Db::Query('INSERT into PendingPayments (Timestamp, ChannelId, TransactionId) values (utc_timestamp(), ?, ?);', [PAYMENT_CHANNEL_FA, $transactionId]);
} }
$log->Write('Donation ID: ' . $transactionId); $log->Write('Donation ID: ' . $transactionId);