Convert newsletter to use Users table as base

This commit is contained in:
Alex Cabal 2022-07-04 12:01:36 -05:00
parent b48a788430
commit 011cd747f1
34 changed files with 444 additions and 307 deletions

View file

@ -256,18 +256,19 @@ Define webroot /standardebooks.org/web
RewriteRule ^/(opds|rss|atom)(.*)$ /feeds/$1$2 [R=301,L]
# Newsletter
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
RewriteRule ^/newsletter$ /newsletter/subscriptions/new.php [L]
RewriteRule ^/newsletter/subscriptions/([^/\.]+?)$ /newsletter/subscriptions/get.php?uuid=$1 [L]
RewriteRule ^/newsletter/subscriptions/([^/\.]+?)/(confirm|delete|success)$ /newsletter/subscriptions/$2.php?uuid=$1 [L]
# 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
RewriteRule ^/patrons-circle/polls/([^/\.]+)$ /patrons-circle/polls/get.php?pollurlname=$1 [L]
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes/(new|success)$ /patrons-circle/polls/votes/$2.php?pollurlname=$1 [L]
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1 [L]
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1 [L]
</VirtualHost>
<VirtualHost *:80>

View file

@ -255,16 +255,17 @@ Define webroot /standardebooks.org/web
RewriteRule ^/(opds|rss|atom)(.*)$ /feeds/$1$2 [R=301,L]
# Newsletter
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
RewriteRule ^/newsletter$ /newsletter/subscriptions/new.php [L]
RewriteRule ^/newsletter/subscriptions/([^/\.]+?)$ /newsletter/subscriptions/get.php?uuid=$1 [L]
RewriteRule ^/newsletter/subscriptions/([^/\.]+?)/(confirm|delete|success)$ /newsletter/subscriptions/$2.php?uuid=$1 [L]
# 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
RewriteRule ^/patrons-circle/polls/([^/\.]+)$ /patrons-circle/polls/get.php?pollurlname=$1 [L]
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes/(new|success)$ /patrons-circle/polls/votes/$2.php?pollurlname=$1 [L]
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes$ /patrons-circle/polls/votes/index.php?pollurlname=$1 [L]
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/patrons-circle/polls/([^/]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1
RewriteRule ^/patrons-circle/polls/([^/\.]+)/votes$ /patrons-circle/polls/votes/post.php?pollurlname=$1 [L]
</VirtualHost>

View file

@ -1,14 +0,0 @@
CREATE TABLE `NewsletterSubscribers` (
`NewsletterSubscriberId` int(10) unsigned NOT NULL AUTO_INCREMENT,
`Email` varchar(80) NOT NULL,
`Uuid` char(36) NOT NULL,
`FirstName` varchar(80) DEFAULT NULL,
`LastName` varchar(80) DEFAULT NULL,
`IsConfirmed` tinyint(1) unsigned NOT NULL DEFAULT 0,
`IsSubscribedToNewsletter` tinyint(1) unsigned NOT NULL DEFAULT 1,
`IsSubscribedToSummary` tinyint(1) unsigned NOT NULL DEFAULT 1,
`Created` datetime NOT NULL,
PRIMARY KEY (`NewsletterSubscriberId`),
UNIQUE KEY `Uuid_UNIQUE` (`Uuid`),
UNIQUE KEY `Email_UNIQUE` (`Email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -0,0 +1,8 @@
CREATE TABLE `NewsletterSubscriptions` (
`UserId` int(10) unsigned NOT NULL,
`IsConfirmed` tinyint(1) unsigned NOT NULL DEFAULT 0,
`IsSubscribedToNewsletter` tinyint(1) unsigned NOT NULL DEFAULT 1,
`IsSubscribedToSummary` tinyint(1) unsigned NOT NULL DEFAULT 1,
`Created` datetime NOT NULL,
PRIMARY KEY (`UserId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -5,6 +5,10 @@ class Db{
return $GLOBALS['DbConnection']->GetLastInsertedId();
}
public static function GetAffectedRowCount(): int{
return $GLOBALS['DbConnection']->LastQueryAffectedRowCount;
}
/**
* @param string $query
* @param array<mixed> $args

View file

@ -6,6 +6,7 @@ class DbConnection{
private $_link = null;
public $IsConnected = false;
public $QueryCount = 0;
public $LastQueryAffectedRowCount = 0;
public function __construct(?string $defaultDatabase = null, string $host = 'localhost', ?string $user = null, string$password = '', bool $forceUtf8 = true, bool $require = true){
if($user === null){
@ -160,6 +161,8 @@ class DbConnection{
private function ExecuteQuery(PDOStatement $handle, string $class = 'stdClass'): array{
$handle->execute();
$this->LastQueryAffectedRowCount = $handle->rowCount();
$result = [];
do{
try{

View file

@ -59,7 +59,11 @@ class Ebook{
public $TextSinglePageUrl;
public $TocEntries = null; // A list of non-Roman ToC entries ONLY IF the work has the 'se:is-a-collection' metadata element, null otherwise
public function __construct(string $wwwFilesystemPath){
public function __construct(?string $wwwFilesystemPath = null){
if($wwwFilesystemPath === null){
return;
}
// First, construct a source repo path from our WWW filesystem path.
$this->RepoFilesystemPath = str_replace(EBOOKS_DIST_PATH, '', $wwwFilesystemPath);
$this->RepoFilesystemPath = SITE_ROOT . '/ebooks/' . str_replace('/', '_', $this->RepoFilesystemPath) . '.git';

View file

@ -1,6 +1,6 @@
<?
namespace Exceptions;
class InvalidNewsletterSubscriberException extends SeException{
class InvalidNewsletterSubscriptionException extends SeException{
protected $message = 'We couldnt find you in our newsletter subscribers list.';
}

View file

@ -1,6 +1,6 @@
<?
namespace Exceptions;
class NewsletterSubscriberExistsException extends SeException{
class NewsletterSubscriptionExistsException extends SeException{
protected $message = 'Youre already subscribed to the newsletter.';
}

View file

@ -1,107 +0,0 @@
<?
use Safe\DateTime;
use Ramsey\Uuid\Uuid;
/**
* @property string $Url
*/
class NewsletterSubscriber extends PropertiesBase{
public $NewsletterSubscriberId;
public $Uuid;
public $Email;
public $FirstName;
public $LastName;
public $IsConfirmed = false;
public $IsSubscribedToSummary = true;
public $IsSubscribedToNewsletter = true;
public $Created;
protected $_Url = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{
if($this->_Url === null){
$this->_Url = SITE_URL . '/newsletter/subscribers/' . $this->Uuid;
}
return $this->_Url;
}
// *******
// METHODS
// *******
public function Create(): void{
$this->Validate();
$uuid = Uuid::uuid4();
$this->Uuid = $uuid->toString();
$this->Created = new DateTime();
try{
Db::Query('INSERT into NewsletterSubscribers (Email, Uuid, FirstName, LastName, IsConfirmed, IsSubscribedToNewsletter, IsSubscribedToSummary, Created) values (?, ?, ?, ?, ?, ?, ?, ?);', [$this->Email, $this->Uuid, $this->FirstName, $this->LastName, false, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary, $this->Created]);
}
catch(PDOException $ex){
if($ex->errorInfo[1] == 1062){
// Duplicate unique key; email already in use
throw new Exceptions\NewsletterSubscriberExistsException();
}
else{
throw $ex;
}
}
$this->NewsletterSubscriberId = Db::GetLastInsertedId();
// Send the double opt-in confirmation email
$em = new Email(true);
$em->PostmarkStream = EMAIL_POSTMARK_STREAM_BROADCAST;
$em->To = $this->Email;
$em->Subject = 'Action required: confirm your newsletter subscription';
$em->Body = Template::EmailNewsletterConfirmation(['subscriber' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]);
$em->TextBody = Template::EmailNewsletterConfirmationText(['subscriber' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]);
$em->Send();
}
public function Confirm(): void{
Db::Query('UPDATE NewsletterSubscribers set IsConfirmed = true where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
}
public function Delete(): void{
Db::Query('DELETE from NewsletterSubscribers where NewsletterSubscriberId = ?;', [$this->NewsletterSubscriberId]);
}
public function Validate(): void{
$error = new Exceptions\ValidationException();
if($this->Email == '' || !filter_var($this->Email, FILTER_VALIDATE_EMAIL)){
$error->Add(new Exceptions\InvalidEmailException());
}
if(!$this->IsSubscribedToSummary && !$this->IsSubscribedToNewsletter){
$error->Add(new Exceptions\NewsletterRequiredException());
}
if($error->HasExceptions){
throw $error;
}
}
// ***********
// ORM METHODS
// ***********
public static function Get(string $uuid): NewsletterSubscriber{
$subscribers = Db::Query('SELECT * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
if(sizeof($subscribers) == 0){
throw new Exceptions\InvalidNewsletterSubscriberException();
}
return $subscribers[0];
}
}

View file

@ -0,0 +1,123 @@
<?
use Safe\DateTime;
/**
* @property User $User
* @property string $Url
*/
class NewsletterSubscription extends PropertiesBase{
public $IsConfirmed = false;
public $IsSubscribedToSummary;
public $IsSubscribedToNewsletter;
public $UserId;
protected $_User;
public $Created;
protected $_Url = null;
// *******
// GETTERS
// *******
protected function GetUrl(): string{
if($this->_Url === null){
$this->_Url = '/newsletter/subscriptions/' . $this->User->Uuid;
}
return $this->_Url;
}
// *******
// METHODS
// *******
public function Create(): void{
$this->Validate();
// Do we need to create a user?
try{
$this->User = User::GetByEmail($this->User->Email);
}
catch(Exceptions\InvalidUserException $ex){
// User doesn't exist, create the user
$this->User->Create();
}
$this->UserId = $this->User->UserId;
$this->Created = new DateTime();
try{
Db::Query('INSERT into NewsletterSubscriptions (UserId, IsConfirmed, IsSubscribedToNewsletter, IsSubscribedToSummary, Created) values (?, ?, ?, ?, ?);', [$this->User->UserId, false, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary, $this->Created]);
}
catch(PDOException $ex){
if($ex->errorInfo[1] == 1062){
// Duplicate unique key; email already in use
throw new Exceptions\NewsletterSubscriptionExistsException();
}
else{
throw $ex;
}
}
// Send the double opt-in confirmation email
$this->SendConfirmationEmail();
}
public function Save(): void{
$this->Validate();
Db::Query('UPDATE NewsletterSubscriptions set IsConfirmed = ?, IsSubscribedToNewsletter = ?, IsSubscribedToSummary = ? where UserId = ?', [$this->IsConfirmed, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary, $this->UserId]);
}
public function SendConfirmationEmail(): void{
$em = new Email(true);
$em->PostmarkStream = EMAIL_POSTMARK_STREAM_BROADCAST;
$em->To = $this->User->Email;
if($this->User->Name != ''){
$em->ToName = $this->User->Name;
}
$em->Subject = 'Action required: confirm your newsletter subscription';
$em->Body = Template::EmailNewsletterConfirmation(['subscription' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]);
$em->TextBody = Template::EmailNewsletterConfirmationText(['subscription' => $this, 'isSubscribedToSummary' => $this->IsSubscribedToSummary, 'isSubscribedToNewsletter' => $this->IsSubscribedToNewsletter]);
$em->Send();
}
public function Confirm(): void{
Db::Query('UPDATE NewsletterSubscriptions set IsConfirmed = true where UserId = ?;', [$this->UserId]);
}
public function Delete(): void{
Db::Query('DELETE from NewsletterSubscriptions where UserId = ?;', [$this->UserId]);
}
public function Validate(): void{
$error = new Exceptions\ValidationException();
if($this->User === null || $this->User->Email == '' || !filter_var($this->User->Email, FILTER_VALIDATE_EMAIL)){
$error->Add(new Exceptions\InvalidEmailException());
}
if(!$this->IsSubscribedToSummary && !$this->IsSubscribedToNewsletter){
$error->Add(new Exceptions\NewsletterRequiredException());
}
if($error->HasExceptions){
throw $error;
}
}
// ***********
// ORM METHODS
// ***********
public static function Get(string $uuid): NewsletterSubscription{
$result = Db::Query('SELECT ns.* from NewsletterSubscriptions ns inner join Users u on ns.UserId = u.UserId where u.Uuid = ?', [$uuid], 'NewsletterSubscription');
if(sizeof($result) == 0){
throw new Exceptions\InvalidNewsletterSubscriptionException();
}
return $result[0];
}
}

View file

@ -33,4 +33,10 @@ class Template{
return self::Get($function, $arguments);
}
}
public static function Emit404(): void{
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
}

View file

@ -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(), Created) >= 7');
Db::Query('DELETE from NewsletterSubscriptions where IsConfirmed = false and datediff(utc_timestamp(), Created) >= 7');
?>

View file

@ -79,6 +79,7 @@ $letterhead = $letterhead ?? false;
h2{
font-family: "League Spartan", "Helvetica", "Arial", sans-serif;
font-weight: bold;
hyphens: none;
margin: 1em auto;
text-align: center;
}
@ -110,7 +111,6 @@ $letterhead = $letterhead ?? false;
}
address{
font-size: .75em;
text-transform: none;
}
@ -124,6 +124,7 @@ $letterhead = $letterhead ?? false;
.footer{
border-top: 1px solid #ccc;
font-size: .75em;
margin-top: 2em;
padding-top: 2em;
text-align: center;
@ -135,7 +136,7 @@ $letterhead = $letterhead ?? false;
}
.footer img{
margin-top: 1em;
margin-top: 2em;
max-width: 55px;
}

View file

@ -8,7 +8,7 @@
</ul>
<p>Please use the button below to confirm your subscription—you wont receive email from us until you do.</p>
<p class="button-row">
<a href="<?= $subscriber->Url ?>/confirm" class="button">Yes, confirm my subscription</a>
<a href="<?= SITE_URL . $subscription->Url ?>/confirm" class="button">Yes, confirm my subscription</a>
</p>
<p>If you didnt subscribe, or youre not sure why you received this email, you can safely delete it and you wont receive any more email from us.</p>
<?= Template::EmailFooter() ?>

View file

@ -8,9 +8,10 @@ You subscribed to:
<? } ?>
<? if($isSubscribedToSummary){ ?>- A monthly summary of new ebook releases
<? } ?>
Please use the link below to confirm your subscription—you wont receive email from us until you do.
<<?= $subscriber->Url ?>/confirm>
<<?= SITE_URL . $subscription->Url ?>/confirm>
If you didnt subscribe, or youre not sure why you received this email, you can safely delete it and you wont receive any more email from us.

View file

@ -13,7 +13,7 @@ else{
$exceptions[] = $exception;
}
?>
<ul class="error">
<ul class="message error">
<? foreach($exceptions as $ex){ ?>
<li>
<p><? $message = $ex->getMessage(); if($message == ''){ $message = 'An error occurred.'; } ?><?= str_replace('CAPTCHA', '<abbr class="acronym">CAPTCHA</abbr>', Formatter::ToPlainText($message)) ?></p>

View file

@ -625,17 +625,29 @@ input[type="checkbox"]:focus{
outline: none;
}
ul.error{
border: 2px solid #c33b3b;
.message{
border: 2px solid rgba(0, 0, 0, .25);
border-radius: .25rem;
background: #e14646;
padding: 1rem;
color: #fff;
text-shadow: 1px 1px 0px rgba(0, 0, 0, .75);
}
ul.error li:only-child{
.message.error{
background: #c45d5d;
}
.message.success{
background: #5dc47c;
}
ul.message.error li{
margin-left: 1rem;
}
ul.message.error li:only-child{
list-style: none;
margin-left: 0;
}
.ebooks nav li.highlighted a:focus,
@ -2058,7 +2070,7 @@ article.ebook h2 + section > h3:first-of-type{
}
form[action*="/polls/"],
form[action="/newsletter/subscribers"]{
form[action="/newsletter/subscriptions"]{
display: grid;
grid-gap: 2rem;
grid-template-columns: 1fr 1fr;
@ -2067,18 +2079,18 @@ form[action="/newsletter/subscribers"]{
}
form[action*="/polls/"] label.email,
form[action="/newsletter/subscribers"] label.email,
form[action="/newsletter/subscribers"] label.captcha{
form[action="/newsletter/subscriptions"] label.email,
form[action="/newsletter/subscriptions"] label.captcha{
grid-column: 1 / span 2;
}
form[action="/newsletter/subscribers"] label.captcha div{
form[action="/newsletter/subscriptions"] label.captcha div{
display: grid;
grid-gap: 1rem;
grid-template-columns: 1fr 1fr;
}
form[action="/newsletter/subscribers"] label.captcha div input{
form[action="/newsletter/subscriptions"] label.captcha div input{
align-self: center;
}
@ -2087,14 +2099,14 @@ form fieldset ul{
}
form[action*="/polls/"] button,
form[action="/newsletter/subscribers"] button{
form[action="/newsletter/subscriptions"] button{
grid-column: 2;
justify-self: end;
margin-left: 0;
}
form[action*="/polls/"] fieldset,
form[action="/newsletter/subscribers"] fieldset{
form[action="/newsletter/subscriptions"] fieldset{
grid-column: 1 / span 2;
}
@ -2967,9 +2979,9 @@ ul.feed p{
margin-top: 0;
}
form[action="/newsletter/subscribers"] label.text,
form[action="/newsletter/subscribers"] label.captcha,
form[action="/newsletter/subscribers"] fieldset{
form[action="/newsletter/subscriptions"] label.text,
form[action="/newsletter/subscriptions"] label.captcha,
form[action="/newsletter/subscriptions"] fieldset{
grid-column: 1 / span 2;
}

View file

@ -1,6 +1,8 @@
<?
require_once('Core.php');
$ebooks = [];
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/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
@ -17,9 +19,7 @@ try{
}
}
catch(Exceptions\InvalidAuthorException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => 'Ebooks by ' . strip_tags($ebooks[0]->AuthorsHtml), 'highlight' => 'ebooks', 'description' => 'All of the Standard Ebooks ebooks by ' . strip_tags($ebooks[0]->AuthorsHtml)]) ?>
<main class="ebooks">

View file

@ -8,6 +8,13 @@ use function Safe\preg_replace;
use function Safe\apcu_fetch;
use function Safe\shuffle;
$ebook = new Ebook();
$transcriptionSources = [];
$scanSources = [];
$otherSources = [];
$carousel = [];
$carouselTag = null;
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/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
@ -42,9 +49,6 @@ try{
}
// Divide our sources into transcriptions and scans
$transcriptionSources = [];
$scanSources = [];
$otherSources = [];
foreach($ebook->Sources as $source){
switch($source->Type){
case SOURCE_PROJECT_GUTENBERG:
@ -69,9 +73,7 @@ try{
// Generate the bottom carousel.
// Pick a random tag from this ebook, and get ebooks in the same tag
$carousel = [];
$ebooks = [];
$carouselTag = null;
if(sizeof($ebook->Tags) > 0){
$carouselTag = $ebook->Tags[rand(0, sizeof($ebook->Tags) - 1)];
$ebooks = Library::GetEbooksByTag(strtolower($carouselTag->Name));
@ -104,9 +106,7 @@ catch(Exceptions\SeeOtherEbookException $ex){
exit();
}
catch(Exceptions\InvalidEbookException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => strip_tags($ebook->TitleWithCreditsHtml) . ' - Free ebook download', 'ogType' => 'book', 'coverUrl' => $ebook->DistCoverUrl, 'highlight' => 'ebooks', 'description' => 'Free epub ebook download of the Standard Ebooks edition of ' . $ebook->Title . ': ' . $ebook->Description]) ?>
<main>

View file

@ -14,6 +14,10 @@ try{
$pages = 0;
$totalEbooks = 0;
$collectionObject = null;
$pageDescription = '';
$pageTitle = '';
$pageHeader = '';
$queryString = '';
if($page <= 0){
$page = 1;
@ -90,9 +94,8 @@ try{
if($page > 1){
$pageTitle .= ', page ' . $page;
}
$pageDescription = 'Page ' . $page . ' of the Standard Ebooks free ebook library';
$queryString = '';
$pageDescription = 'Page ' . $page . ' of the Standard Ebooks free ebook library';
if($collection === null){
if($query != ''){
@ -119,9 +122,7 @@ try{
$queryString = preg_replace('/^&amp;/ius', '', $queryString);
}
catch(Exceptions\InvalidCollectionException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?>
<main class="ebooks">

View file

@ -1,21 +0,0 @@
<?
require_once('Core.php');
try{
$subscriber = NewsletterSubscriber::Get(HttpInput::Str(GET, 'uuid') ?? '');
$subscriber->Confirm();
}
catch(Exceptions\InvalidNewsletterSubscriberException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
}
?><?= Template::Header(['title' => 'Your subscription to the Standard Ebooks newsletter has been confirmed', 'highlight' => 'newsletter', 'description' => 'Your subscription to the Standard Ebooks newsletter has been confirmed.']) ?>
<main>
<article>
<h1>Your subscription is confirmed!</h1>
<p>Thank you! Youll now receive Standard Ebooks email newsletters.</p>
<p>To unsubscribe, simply follow the link at the bottom of any of our newsletters, or <a href="<?= $subscriber->Url ?>/delete">click here</a>.</p>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -1,73 +0,0 @@
<?
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();
$subscriber = new NewsletterSubscriber();
if(HttpInput::Str(POST, 'automationtest', false)){
// A bot filled out this form field, which should always be empty. Pretend like we succeeded.
if($requestType == WEB){
http_response_code(303);
header('Location: /newsletter/subscribers/success');
}
else{
// Access via REST api; 201 CREATED with location
http_response_code(201);
header('Location: ' . $subscriber->Url);
}
exit();
}
try{
$subscriber->FirstName = HttpInput::Str(POST, 'firstname', false);
$subscriber->LastName = HttpInput::Str(POST, 'lastname', false);
$subscriber->Email = HttpInput::Str(POST, 'email', false);
$subscriber->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'newsletter', false);
$subscriber->IsSubscribedToSummary = HttpInput::Bool(POST, 'monthlysummary', false);
$captcha = $_SESSION['captcha'] ?? '';
if($captcha === '' || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false) ?? '')){
throw new Exceptions\ValidationException(new Exceptions\InvalidCaptchaException());
}
$subscriber->Create();
session_unset();
if($requestType == WEB){
http_response_code(303);
header('Location: /newsletter/subscribers/success');
}
else{
// Access via REST api; 201 CREATED with location
http_response_code(201);
header('Location: ' . $subscriber->Url);
}
}
catch(Exceptions\SeException $ex){
// Validation failed
if($requestType == WEB){
$_SESSION['subscriber'] = $subscriber;
$_SESSION['exception'] = $ex;
// Access via form; 303 redirect to the form, which will emit a 400 BAD REQUEST
http_response_code(303);
header('Location: /newsletter/subscribers/new');
}
else{
// Access via REST api; 400 BAD REQUEST
http_response_code(400);
}
}

View file

@ -1,12 +0,0 @@
<?
require_once('Core.php');
?><?= Template::Header(['title' => 'Subscribe to the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'Subscribe to the Standard Ebooks newsletter to receive occasional updates about the project.']) ?>
<main>
<article>
<h1>Almost done!</h1>
<p>Please check your email inbox for a confirmation email containing a link to finalize your subscription to our newsletter.</p>
<p>Your subscription wont be activated until you click that link—this helps us prevent spam. Thank you!</p>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,21 @@
<?
require_once('Core.php');
session_start();
$subscription = new NewsletterSubscription();
try{
$subscription = NewsletterSubscription::Get(HttpInput::Str(GET, 'uuid') ?? '');
if(!$subscription->IsConfirmed){
$subscription->Confirm();
$_SESSION['subscription-confirmed'] = $subscription->UserId;
}
http_response_code(303);
header('Location: ' . $subscription->Url);
}
catch(Exceptions\InvalidNewsletterSubscriptionException $ex){
Template::Emit404();
}

View file

@ -11,8 +11,8 @@ try{
throw new Exceptions\InvalidRequestException();
}
$subscriber = NewsletterSubscriber::Get(HttpInput::Str(GET, 'uuid') ?? '');
$subscriber->Delete();
$subscription = NewsletterSubscription::Get(HttpInput::Str(GET, 'uuid') ?? '');
$subscription->Delete();
if($requestType == REST){
exit();
@ -22,12 +22,14 @@ catch(Exceptions\InvalidRequestException $ex){
http_response_code(405);
exit();
}
catch(Exceptions\InvalidNewsletterSubscriberException $ex){
http_response_code(404);
catch(Exceptions\InvalidNewsletterSubscriptionException $ex){
if($requestType == WEB){
include(WEB_ROOT . '/404.php');
Template::Emit404();
}
else{
http_response_code(404);
exit();
}
exit();
}
?><?= Template::Header(['title' => 'Youve unsubscribed from the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'Youve unsubscribed from the Standard Ebooks newsletter.']) ?>

View file

@ -0,0 +1,70 @@
<?
require_once('Core.php');
session_start();
$subscription = new NewsletterSubscription();
$created = false;
$updated = false;
$confirmed = false;
try{
if(isset($_SESSION['subscription-created']) && $_SESSION['subscription-created'] == 0){
$created = true;
}
else{
$subscription = NewsletterSubscription::Get(HttpInput::Str(GET, 'uuid', false) ?? '');
if(isset($_SESSION['subscription-created']) && $_SESSION['subscription-created'] == $subscription->UserId){
$created = true;
}
if(isset($_SESSION['subscription-updated']) && $_SESSION['subscription-updated'] == $subscription->UserId){
$updated = true;
}
if(isset($_SESSION['subscription-confirmed']) && $_SESSION['subscription-confirmed'] == $subscription->UserId){
$confirmed = true;
}
}
if($created || $updated || $confirmed){
session_unset();
}
if($created){
// HTTP 201 Created
http_response_code(201);
}
}
catch(Exceptions\SeException $ex){
Template::Emit404();
}
?><?= Template::Header(['title' => 'Your subscription to the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'Your subscription to the Standard Ebooks newsletter.']) ?>
<main>
<section>
<? if($subscription->IsConfirmed){ ?>
<h1>Your Standard Ebooks Newsletter Subscription</h1>
<? if($updated){ ?>
<p class="message success">Your settings have been saved!</p>
<? } ?>
<? if($confirmed){ ?>
<p class="message success">Your subscription has been confirmed!</p>
<? } ?>
<p>Youre set to receive the following newsletters:</p>
<ul>
<? if($subscription->IsSubscribedToSummary){ ?><li><p>A monthly summary of new ebook releases</p></li><? } ?>
<? if($subscription->IsSubscribedToNewsletter){ ?><li><p>The occasional Standard Ebooks newsletter</p></li><? } ?>
</ul>
<p class="button-row narrow">
<a href="<?= $subscription->Url ?>/delete" class="button">Unsubscribe</a>
</p>
<? }else{ ?>
<h1>Almost done!</h1>
<p>Please check your email inbox for a confirmation email containing a link to finalize your subscription to our newsletter.</p>
<p>Your subscription wont be activated until you click that link—this helps us prevent spam. Thank you!</p>
<? } ?>
</section>
</main>
<?= Template::Footer() ?>

View file

@ -5,7 +5,7 @@ use function Safe\session_unset;
session_start();
$subscriber = $_SESSION['subscriber'] ?? new NewsletterSubscriber();
$subscription = $_SESSION['subscription'] ?? new NewsletterSubscription();
$exception = $_SESSION['exception'] ?? null;
if($exception){
@ -29,12 +29,12 @@ if($exception){
<?= Template::Error(['exception' => $exception]) ?>
<form action="/newsletter/subscribers" method="post">
<form action="/newsletter/subscriptions" method="post">
<label class="automation-test"><? /* Test for spam bots filling out all fields */ ?>
<input type="text" name="automationtest" value="" maxlength="80" />
</label>
<label class="email">Your email address
<input type="email" name="email" value="<?= Formatter::ToPlainText($subscriber->Email) ?>" maxlength="80" required="required" />
<input type="email" name="email" value="<? if($subscription->User !== null){ ?><?= Formatter::ToPlainText($subscription->User->Email) ?><? } ?>" maxlength="80" required="required" />
</label>
<label class="captcha">
Type the letters in the <abbr class="acronym">CAPTCHA</abbr> image
@ -47,10 +47,10 @@ if($exception){
<p>What kind of email would you like to receive?</p>
<ul>
<li>
<label class="checkbox"><input type="checkbox" value="1" name="newsletter"<? if($subscriber->IsSubscribedToNewsletter){ ?> checked="checked"<? } ?> />The occasional Standard Ebooks newsletter</label>
<label class="checkbox"><input type="checkbox" value="1" name="issubscribedtonewsletter"<? if($subscription->IsSubscribedToNewsletter){ ?> checked="checked"<? } ?> />The occasional Standard Ebooks newsletter</label>
</li>
<li>
<label class="checkbox"><input type="checkbox" value="1" name="monthlysummary"<? if($subscriber->IsSubscribedToSummary){ ?> checked="checked"<? } ?> />A monthly summary of new ebook releases</label>
<label class="checkbox"><input type="checkbox" value="1" name="issubscribedtosummary"<? if($subscription->IsSubscribedToSummary){ ?> checked="checked"<? } ?> />A monthly summary of new ebook releases</label>
</li>
</ul>
</fieldset>

View file

@ -0,0 +1,114 @@
<?
require_once('Core.php');
use Ramsey\Uuid\Uuid;
use function Safe\session_unset;
if(HttpInput::RequestMethod() != HTTP_POST){
http_response_code(405);
exit();
}
session_start();
$requestType = HttpInput::RequestType();
$subscription = new NewsletterSubscription();
if(HttpInput::Str(POST, 'automationtest', false)){
// A bot filled out this form field, which should always be empty. Pretend like we succeeded.
if($requestType == WEB){
http_response_code(303);
$uuid = Uuid::uuid4();
$subscription->User = new User();
$subscription->User->Uuid = $uuid->toString();
$_SESSION['subscription-created'] = 0; // 0 means 'bot'
header('Location: ' . $subscription->Url);
}
else{
// Access via REST api; 201 CREATED with location
http_response_code(201);
header('Location: ' . $subscription->Url);
}
exit();
}
try{
$subscription->User = new User();
$subscription->User->Email = HttpInput::Str(POST, 'email', false);
$subscription->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'issubscribedtonewsletter', false);
$subscription->IsSubscribedToSummary = HttpInput::Bool(POST, 'issubscribedtosummary', false);
$captcha = $_SESSION['captcha'] ?? '';
$exception = new Exceptions\ValidationException();
try{
$subscription->Validate();
}
catch(Exceptions\ValidationException $ex){
$exception->Add($ex);
}
if($captcha === '' || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false) ?? '')){
$exception->Add(new Exceptions\InvalidCaptchaException());
}
if($exception->HasExceptions){
throw $exception;
}
$subscription->Create();
session_unset();
if($requestType == WEB){
http_response_code(303);
$_SESSION['subscription-created'] = $subscription->UserId;
header('Location: ' . $subscription->Url);
}
else{
// Access via REST api; 201 CREATED with location
http_response_code(201);
header('Location: ' . $subscription->Url);
}
}
catch(Exceptions\NewsletterSubscriptionExistsException $ex){
// Subscription exists.
if($requestType == WEB){
// If we're accessing from the web, update the subscription,
// re-sending the confirmation email if the user isn't yet confirmed
$existingSubscription = NewsletterSubscription::Get($subscription->User->Uuid);
$subscription->IsConfirmed = $existingSubscription->IsConfirmed;
$subscription->Save();
// Don't re-send the email after all, to prevent spam
// if(!$subscription->IsConfirmed){
// $subscription->SendConfirmationEmail();
// }
http_response_code(303);
$_SESSION['subscription-updated'] = $subscription->UserId;
header('Location: ' . $subscription->Url);
}
else{
// Access via REST api; 409 CONFLICT
http_response_code(409);
}
}
catch(Exceptions\SeException $ex){
// Validation failed
if($requestType == WEB){
$_SESSION['subscription'] = $subscription;
$_SESSION['exception'] = $ex;
// Access via form; 303 redirect to the form, which will emit a 400 BAD REQUEST
http_response_code(303);
header('Location: /newsletter/subscriptions/new');
}
else{
// Access via REST api; 400 BAD REQUEST
http_response_code(400);
}
}

View file

@ -3,15 +3,13 @@ require_once('Core.php');
use Safe\DateTime;
$poll = null;
$poll = new Poll();
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => $poll->Name, 'highlight' => '', 'description' => $poll->Description]) ?>

View file

@ -1,15 +1,13 @@
<?
require_once('Core.php');
$poll = null;
$poll = new Poll();
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => 'Results for the ' . $poll->Name . ' poll', 'highlight' => '', 'description' => 'The voting results for the ' . $poll->Name . ' poll.']) ?>

View file

@ -8,15 +8,13 @@ session_start();
$vote = $_SESSION['vote'] ?? new Vote();
$exception = $_SESSION['exception'] ?? null;
$poll = null;
$poll = new Poll();
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
if($exception){

View file

@ -1,15 +1,13 @@
<?
require_once('Core.php');
$poll = null;
$poll = new Poll();
try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
}
catch(Exceptions\SeException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();
Template::Emit404();
}
?><?= Template::Header(['title' => 'Thank you for voting!', 'highlight' => 'newsletter', 'description' => 'Thank you for voting in a Standard Ebooks poll!']) ?>

View file

@ -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 ns.* from NewsletterSubscriptions ns inner join Users u on ns.UserId = u.UserId where u.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 ns.* from NewsletterSubscriptions ns inner join Users u on ns.UserId = u.UserId where u.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();