mirror of
https://github.com/standardebooks/web.git
synced 2025-07-05 22:30:30 -04:00
Add poll system for Patrons Circle
This commit is contained in:
parent
3555d53615
commit
2ef5ce6551
44 changed files with 717 additions and 98 deletions
|
@ -258,6 +258,16 @@ Define webroot /standardebooks.org/web
|
|||
# Newsletter
|
||||
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
|
||||
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 *:80>
|
||||
|
|
|
@ -257,4 +257,14 @@ Define webroot /standardebooks.org/web
|
|||
# Newsletter
|
||||
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
|
||||
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>
|
||||
|
|
|
@ -19,6 +19,12 @@ parameters:
|
|||
- '#Method Ebook::NullIfEmpty\(\) has parameter \$elements with no type specified.#'
|
||||
- '#Method HttpInput::GetHttpVar\(\) has no return 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:
|
||||
7
|
||||
paths:
|
||||
|
|
8
config/sql/se/PollItems.sql
Normal file
8
config/sql/se/PollItems.sql
Normal 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
11
config/sql/se/Polls.sql
Normal 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
6
config/sql/se/Votes.sql
Normal 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;
|
|
@ -59,6 +59,13 @@ const HTTP_VAR_BOOL = 2;
|
|||
const HTTP_VAR_DEC = 3;
|
||||
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_LIST = 'list';
|
||||
|
||||
|
|
6
lib/Exceptions/InvalidPatronException.php
Normal file
6
lib/Exceptions/InvalidPatronException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidPatronException extends SeException{
|
||||
protected $message = 'We couldn’t locate you in the Patrons Circle.';
|
||||
}
|
6
lib/Exceptions/InvalidPollException.php
Normal file
6
lib/Exceptions/InvalidPollException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidPollException extends SeException{
|
||||
protected $message = 'We couldn’t locate that poll.';
|
||||
}
|
6
lib/Exceptions/InvalidPollItemException.php
Normal file
6
lib/Exceptions/InvalidPollItemException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidPollItemException extends SeException{
|
||||
protected $message = 'We couldn’t locate that poll item.';
|
||||
}
|
6
lib/Exceptions/InvalidUserException.php
Normal file
6
lib/Exceptions/InvalidUserException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidUserException extends SeException{
|
||||
protected $message = 'We couldn’t locate you in our system.';
|
||||
}
|
6
lib/Exceptions/PollClosedException.php
Normal file
6
lib/Exceptions/PollClosedException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class PollClosedException extends SeException{
|
||||
protected $message = 'This poll is not open to voting right now.';
|
||||
}
|
6
lib/Exceptions/PollItemRequiredException.php
Normal file
6
lib/Exceptions/PollItemRequiredException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class PollItemRequiredException extends SeException{
|
||||
protected $message = 'You must select an item to vote on.';
|
||||
}
|
|
@ -8,6 +8,12 @@ class ValidationException extends SeException{
|
|||
public $HasExceptions = false;
|
||||
public $IsFatal = false;
|
||||
|
||||
public function __construct(?\Exception $exception = null){
|
||||
if($exception !== null){
|
||||
$this->Add($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string{
|
||||
$output = '';
|
||||
foreach($this->Exceptions as $exception){
|
||||
|
|
6
lib/Exceptions/VoteExistsException.php
Normal file
6
lib/Exceptions/VoteExistsException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class VoteExistsException extends SeException{
|
||||
protected $message = 'You’ve already voted in this poll.';
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<?
|
||||
use Safe\DateTime;
|
||||
use function Safe\file_get_contents;
|
||||
use function Safe\file_put_contents;
|
||||
use function Safe\tempnam;
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
<?
|
||||
use function Safe\preg_match;
|
||||
|
||||
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{
|
||||
$var = self::GetHttpVar($variable, HTTP_VAR_STR, $type, $default);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?
|
||||
use Safe\DateTime;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class NewsletterSubscriber extends PropertiesBase{
|
||||
|
@ -26,9 +27,10 @@ class NewsletterSubscriber extends PropertiesBase{
|
|||
|
||||
$uuid = Uuid::uuid4();
|
||||
$this->Uuid = $uuid->toString();
|
||||
$this->Timestamp = new DateTime();
|
||||
|
||||
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){
|
||||
if($ex->errorInfo[1] == 1062){
|
||||
|
@ -53,11 +55,11 @@ class NewsletterSubscriber extends PropertiesBase{
|
|||
}
|
||||
|
||||
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{
|
||||
Db::Query('delete from NewsletterSubscribers where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
|
||||
Db::Query('DELETE from NewsletterSubscribers where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
|
||||
}
|
||||
|
||||
public function Validate(): void{
|
||||
|
@ -77,11 +79,7 @@ class NewsletterSubscriber extends PropertiesBase{
|
|||
}
|
||||
|
||||
public static function Get(string $uuid): NewsletterSubscriber{
|
||||
if($uuid == ''){
|
||||
throw new Exceptions\InvalidNewsletterSubscriberException();
|
||||
}
|
||||
|
||||
$subscribers = Db::Query('select * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
|
||||
$subscribers = Db::Query('SELECT * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
|
||||
|
||||
if(sizeof($subscribers) == 0){
|
||||
throw new Exceptions\InvalidNewsletterSubscriberException();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?
|
||||
use Safe\DateTime;
|
||||
use function Safe\file_put_contents;
|
||||
|
||||
class OpdsFeed extends AtomFeed{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -10,22 +10,30 @@ class Patron extends PropertiesBase{
|
|||
public $Timestamp = null;
|
||||
public $DeactivatedTimestamp = null;
|
||||
|
||||
public static function Get(int $userId): ?Patron{
|
||||
$result = Db::Query('select * from Patrons where UserId = ?', [$userId], 'Patron');
|
||||
public static function Get(?int $userId): Patron{
|
||||
$result = Db::Query('SELECT * from Patrons where UserId = ?', [$userId], 'Patron');
|
||||
|
||||
return $result[0] ?? null;
|
||||
}
|
||||
|
||||
protected function GetUser(): ?User{
|
||||
if($this->User === null && $this->UserId !== null){
|
||||
$this->User = User::Get($this->UserId);
|
||||
if(sizeof($result) == 0){
|
||||
throw new Exceptions\InvalidPatronException();
|
||||
}
|
||||
|
||||
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{
|
||||
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){
|
||||
$this->SendWelcomeEmail();
|
||||
|
@ -33,7 +41,7 @@ class Patron extends PropertiesBase{
|
|||
}
|
||||
|
||||
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->DeactivatedTimestamp = null;
|
||||
|
||||
|
@ -43,7 +51,7 @@ class Patron extends PropertiesBase{
|
|||
}
|
||||
|
||||
private function SendWelcomeEmail(): void{
|
||||
$this->GetUser();
|
||||
$this->__get('User');
|
||||
if($this->User !== null){
|
||||
$em = new Email();
|
||||
$em->To = $this->User->Email;
|
||||
|
|
|
@ -11,21 +11,13 @@ class Payment extends PropertiesBase{
|
|||
public $Fee;
|
||||
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{
|
||||
if($this->UserId === null){
|
||||
// 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($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){
|
||||
// User doesn't exist, create it now
|
||||
|
@ -41,7 +33,7 @@ class Payment extends PropertiesBase{
|
|||
}
|
||||
|
||||
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){
|
||||
if($ex->errorInfo[1] == 1062){
|
||||
|
|
88
lib/Poll.php
Normal file
88
lib/Poll.php
Normal 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
27
lib/PollItem.php
Normal 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];
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?
|
||||
use function Safe\substr;
|
||||
|
||||
abstract class PropertiesBase extends OrmBase{
|
||||
abstract class PropertiesBase{
|
||||
/**
|
||||
* @param mixed $var
|
||||
* @return mixed
|
||||
|
@ -12,6 +12,15 @@ abstract class PropertiesBase extends OrmBase{
|
|||
if(method_exists($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'){
|
||||
// If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML.
|
||||
if($this->$var === null){
|
||||
|
|
|
@ -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{
|
||||
public $Description;
|
||||
|
||||
|
|
24
lib/User.php
24
lib/User.php
|
@ -1,5 +1,6 @@
|
|||
<?
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Safe\DateTime;
|
||||
|
||||
class User extends PropertiesBase{
|
||||
public $UserId;
|
||||
|
@ -14,10 +15,24 @@ class User extends PropertiesBase{
|
|||
public $Timestamp;
|
||||
public $Uuid;
|
||||
|
||||
public static function Get(int $userId): ?User{
|
||||
$result = Db::Query('select * from Users where UserId = ?', [$userId], 'User');
|
||||
public static function Get(?int $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{
|
||||
|
@ -31,9 +46,10 @@ class User extends PropertiesBase{
|
|||
public function Create(): void{
|
||||
$uuid = Uuid::uuid4();
|
||||
$this->Uuid = $uuid->toString();
|
||||
$this->Timestamp = new DateTime();
|
||||
|
||||
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){
|
||||
if($ex->errorInfo[1] == 1062){
|
||||
|
|
87
lib/Vote.php
Normal file
87
lib/Vote.php
Normal 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]);
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@
|
|||
require_once('/standardebooks.org/web/lib/Core.php');
|
||||
|
||||
// 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');
|
||||
?>
|
||||
|
|
|
@ -160,9 +160,10 @@ try{
|
|||
if(($payment->IsRecurring && $payment->Amount >= 10 && $payment->Timestamp >= $lastMonth) || ($payment->Amount >= 100 && $payment->Timestamp >= $lastYear)){
|
||||
// This payment is eligible for the Patrons Circle.
|
||||
// Are we already a patron?
|
||||
$patron = Patron::Get($payment->UserId);
|
||||
|
||||
if($patron === null){
|
||||
try{
|
||||
$patron = Patron::Get($payment->UserId);
|
||||
}
|
||||
catch(Exceptions\InvalidPatronException $ex){
|
||||
// Not a patron yet, add them to the Patrons Circle
|
||||
$patron = new Patron();
|
||||
$patron->UserId = $payment->UserId;
|
||||
|
@ -194,7 +195,7 @@ try{
|
|||
else{
|
||||
// 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
|
||||
$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
|
||||
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.');
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ $startDate = new DateTime('2022-07-01');
|
|||
$endDate = new DateTime('2022-07-31');
|
||||
$autoHide = $autoHide ?? 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;
|
||||
$stretchCurrent = 0;
|
||||
$stretchTarget = 20;
|
||||
|
|
|
@ -7,7 +7,7 @@ $anonymousPatronCount = 0;
|
|||
// 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
|
||||
|
||||
$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
|
||||
on p.UserId = u.UserId
|
||||
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;
|
||||
');
|
||||
|
||||
$anonymousPatronCount = Db::Query('select sum(cnt) as AnonymousPatronCount
|
||||
$anonymousPatronCount = Db::Query('SELECT sum(cnt) as AnonymousPatronCount
|
||||
from
|
||||
(
|
||||
(
|
||||
|
|
|
@ -1836,6 +1836,16 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{
|
|||
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{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -2047,14 +2057,16 @@ article.ebook h2 + section > h3:first-of-type{
|
|||
left: -5000px;
|
||||
}
|
||||
|
||||
form[action*="/polls/"],
|
||||
form[action="/newsletter/subscribers"]{
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-gap: 2rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
form[action*="/polls/"] label.email,
|
||||
form[action="/newsletter/subscribers"] label.email,
|
||||
form[action="/newsletter/subscribers"] label.captcha{
|
||||
grid-column: 1 / span 2;
|
||||
|
@ -2070,18 +2082,19 @@ form[action="/newsletter/subscribers"] label.captcha div input{
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
form[action="/newsletter/subscribers"] ul{
|
||||
form fieldset ul{
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
form[action*="/polls/"] button,
|
||||
form[action="/newsletter/subscribers"] button{
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
form[action*="/polls/"] fieldset,
|
||||
form[action="/newsletter/subscribers"] fieldset{
|
||||
margin-top: 1rem;
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
|
@ -2096,15 +2109,25 @@ fieldset p{
|
|||
|
||||
label.checkbox{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label.checkbox input{
|
||||
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{
|
||||
margin-left: 1.2rem;
|
||||
list-style: decimal;
|
||||
|
@ -2130,19 +2153,12 @@ aside header{
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.meter,
|
||||
.progress{
|
||||
position: relative;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.progress > div{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.donation a.button{
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
|
@ -2215,6 +2231,7 @@ aside header{
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
.meter p,
|
||||
.progress p{
|
||||
font-size: 1rem;
|
||||
font-family: "League Spartan", Arial, sans-serif;
|
||||
|
@ -2251,7 +2268,14 @@ aside header{
|
|||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.meter > 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
|
||||
FF bug that causes infinite requsts to stripes.svg */
|
||||
background: url("/images/stripes.svg") transparent;
|
||||
|
@ -2260,6 +2284,7 @@ aside header{
|
|||
z-index: 3;
|
||||
}
|
||||
|
||||
meter,
|
||||
progress{
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
@ -2280,6 +2305,7 @@ progress::-webkit-progress-value{
|
|||
box-shadow: 1px 0 1px rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
meter::-moz-meter-bar,
|
||||
progress::-moz-progress-bar{
|
||||
background: var(--button);
|
||||
box-shadow: 1px 0 1px rgba(0, 0, 0, .25);
|
||||
|
@ -2507,6 +2533,33 @@ ul.feed p{
|
|||
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 */
|
||||
/* For iPad, unset the height so it matches the other elements */
|
||||
select[multiple]{
|
||||
|
@ -3016,6 +3069,17 @@ ul.feed p{
|
|||
form[action="/settings"] select{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.votes tr,
|
||||
.votes tr td{
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.votes tr + tr{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 470px){
|
||||
|
|
|
@ -3,11 +3,11 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\preg_match;
|
||||
|
||||
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST;
|
||||
$requestType = HttpInput::RequestType();
|
||||
|
||||
try{
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,14 @@ require_once('Core.php');
|
|||
use function Safe\preg_match;
|
||||
use function Safe\session_unset;
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||
http_response_code(405);
|
||||
exit();
|
||||
}
|
||||
|
||||
session_start();
|
||||
|
||||
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST;
|
||||
$requestType = HttpInput::RequestType();
|
||||
|
||||
$subscriber = new NewsletterSubscriber();
|
||||
|
||||
|
@ -39,11 +39,7 @@ try{
|
|||
$captcha = $_SESSION['captcha'] ?? null;
|
||||
|
||||
if($captcha === null || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false))){
|
||||
$error = new Exceptions\ValidationException();
|
||||
|
||||
$error->Add(new Exceptions\InvalidCaptchaException());
|
||||
|
||||
throw $error;
|
||||
throw new Exceptions\ValidationException(new Exceptions\InvalidCaptchaException());
|
||||
}
|
||||
|
||||
$subscriber->Create();
|
||||
|
|
3
www/patrons-circle/index.php
Normal file
3
www/patrons-circle/index.php
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?
|
||||
header('Location: /donate#patrons-circle');
|
||||
exit();
|
40
www/patrons-circle/polls/get.php
Normal file
40
www/patrons-circle/polls/get.php
Normal 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() ?>
|
46
www/patrons-circle/polls/votes/index.php
Normal file
46
www/patrons-circle/polls/votes/index.php
Normal 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() ?>
|
58
www/patrons-circle/polls/votes/new.php
Normal file
58
www/patrons-circle/polls/votes/new.php
Normal 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() ?>
|
51
www/patrons-circle/polls/votes/post.php
Normal file
51
www/patrons-circle/polls/votes/post.php
Normal 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);
|
||||
}
|
||||
}
|
23
www/patrons-circle/polls/votes/success.php
Normal file
23
www/patrons-circle/polls/votes/success.php
Normal 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() ?>
|
|
@ -17,7 +17,7 @@ $lastPushHashFlag = '';
|
|||
try{
|
||||
$log->Write('Received GitHub webhook.');
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||
throw new Exceptions\WebhookException('Expected HTTP POST.');
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ try{
|
|||
|
||||
$log->Write('Received Postmark webhook.');
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||
throw new Exceptions\WebhookException('Expected HTTP POST.');
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ try{
|
|||
// Received when a user marks an email as spam
|
||||
$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){
|
||||
// Received when a user clicks Postmark's "Unsubscribe" link in a newsletter email
|
||||
|
@ -45,7 +45,7 @@ try{
|
|||
$email = $post->Recipient;
|
||||
|
||||
// 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
|
||||
$handle = curl_init();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?
|
||||
require_once('Core.php');
|
||||
|
||||
use Safe\DateTime;
|
||||
use function Safe\file_get_contents;
|
||||
use function Safe\preg_match;
|
||||
use function Safe\preg_replace;
|
||||
|
@ -15,7 +16,7 @@ $log = new Log(ZOHO_WEBHOOK_LOG_FILE_PATH);
|
|||
try{
|
||||
$log->Write('Received Zoho webhook.');
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||
throw new Exceptions\WebhookException('Expected HTTP POST.');
|
||||
}
|
||||
|
||||
|
@ -53,7 +54,7 @@ try{
|
|||
$payment->Create();
|
||||
}
|
||||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue