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

View file

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

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(); return $GLOBALS['DbConnection']->GetLastInsertedId();
} }
public static function GetAffectedRowCount(): int{
return $GLOBALS['DbConnection']->LastQueryAffectedRowCount;
}
/** /**
* @param string $query * @param string $query
* @param array<mixed> $args * @param array<mixed> $args

View file

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

View file

@ -59,7 +59,11 @@ class Ebook{
public $TextSinglePageUrl; 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 $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. // First, construct a source repo path from our WWW filesystem path.
$this->RepoFilesystemPath = str_replace(EBOOKS_DIST_PATH, '', $wwwFilesystemPath); $this->RepoFilesystemPath = str_replace(EBOOKS_DIST_PATH, '', $wwwFilesystemPath);
$this->RepoFilesystemPath = SITE_ROOT . '/ebooks/' . str_replace('/', '_', $this->RepoFilesystemPath) . '.git'; $this->RepoFilesystemPath = SITE_ROOT . '/ebooks/' . str_replace('/', '_', $this->RepoFilesystemPath) . '.git';

View file

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

View file

@ -1,6 +1,6 @@
<? <?
namespace Exceptions; namespace Exceptions;
class NewsletterSubscriberExistsException extends SeException{ class NewsletterSubscriptionExistsException extends SeException{
protected $message = 'Youre already subscribed to the newsletter.'; 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); 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'); 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(), 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{ h2{
font-family: "League Spartan", "Helvetica", "Arial", sans-serif; font-family: "League Spartan", "Helvetica", "Arial", sans-serif;
font-weight: bold; font-weight: bold;
hyphens: none;
margin: 1em auto; margin: 1em auto;
text-align: center; text-align: center;
} }
@ -110,7 +111,6 @@ $letterhead = $letterhead ?? false;
} }
address{ address{
font-size: .75em;
text-transform: none; text-transform: none;
} }
@ -124,6 +124,7 @@ $letterhead = $letterhead ?? false;
.footer{ .footer{
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
font-size: .75em;
margin-top: 2em; margin-top: 2em;
padding-top: 2em; padding-top: 2em;
text-align: center; text-align: center;
@ -135,7 +136,7 @@ $letterhead = $letterhead ?? false;
} }
.footer img{ .footer img{
margin-top: 1em; margin-top: 2em;
max-width: 55px; max-width: 55px;
} }

View file

@ -8,7 +8,7 @@
</ul> </ul>
<p>Please use the button below to confirm your subscription—you wont receive email from us until you do.</p> <p>Please use the button below to confirm your subscription—you wont receive email from us until you do.</p>
<p class="button-row"> <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>
<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> <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() ?> <?= Template::EmailFooter() ?>

View file

@ -8,9 +8,10 @@ You subscribed to:
<? } ?> <? } ?>
<? if($isSubscribedToSummary){ ?>- A monthly summary of new ebook releases <? 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. 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. 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; $exceptions[] = $exception;
} }
?> ?>
<ul class="error"> <ul class="message error">
<? foreach($exceptions as $ex){ ?> <? foreach($exceptions as $ex){ ?>
<li> <li>
<p><? $message = $ex->getMessage(); if($message == ''){ $message = 'An error occurred.'; } ?><?= str_replace('CAPTCHA', '<abbr class="acronym">CAPTCHA</abbr>', Formatter::ToPlainText($message)) ?></p> <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; outline: none;
} }
ul.error{ .message{
border: 2px solid #c33b3b; border: 2px solid rgba(0, 0, 0, .25);
border-radius: .25rem; border-radius: .25rem;
background: #e14646;
padding: 1rem; padding: 1rem;
color: #fff; color: #fff;
text-shadow: 1px 1px 0px rgba(0, 0, 0, .75); 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; list-style: none;
margin-left: 0;
} }
.ebooks nav li.highlighted a:focus, .ebooks nav li.highlighted a:focus,
@ -2058,7 +2070,7 @@ article.ebook h2 + section > h3:first-of-type{
} }
form[action*="/polls/"], form[action*="/polls/"],
form[action="/newsletter/subscribers"]{ form[action="/newsletter/subscriptions"]{
display: grid; display: grid;
grid-gap: 2rem; grid-gap: 2rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -2067,18 +2079,18 @@ form[action="/newsletter/subscribers"]{
} }
form[action*="/polls/"] label.email, form[action*="/polls/"] label.email,
form[action="/newsletter/subscribers"] label.email, form[action="/newsletter/subscriptions"] label.email,
form[action="/newsletter/subscribers"] label.captcha{ form[action="/newsletter/subscriptions"] label.captcha{
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }
form[action="/newsletter/subscribers"] label.captcha div{ form[action="/newsletter/subscriptions"] label.captcha div{
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 1rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
form[action="/newsletter/subscribers"] label.captcha div input{ form[action="/newsletter/subscriptions"] label.captcha div input{
align-self: center; align-self: center;
} }
@ -2087,14 +2099,14 @@ form fieldset ul{
} }
form[action*="/polls/"] button, form[action*="/polls/"] button,
form[action="/newsletter/subscribers"] button{ form[action="/newsletter/subscriptions"] 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*="/polls/"] fieldset,
form[action="/newsletter/subscribers"] fieldset{ form[action="/newsletter/subscriptions"] fieldset{
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }
@ -2967,9 +2979,9 @@ ul.feed p{
margin-top: 0; margin-top: 0;
} }
form[action="/newsletter/subscribers"] label.text, form[action="/newsletter/subscriptions"] label.text,
form[action="/newsletter/subscribers"] label.captcha, form[action="/newsletter/subscriptions"] label.captcha,
form[action="/newsletter/subscribers"] fieldset{ form[action="/newsletter/subscriptions"] fieldset{
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }

View file

@ -1,6 +1,8 @@
<? <?
require_once('Core.php'); require_once('Core.php');
$ebooks = [];
try{ try{
$urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/ $urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook $wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
@ -17,9 +19,7 @@ try{
} }
} }
catch(Exceptions\InvalidAuthorException $ex){ catch(Exceptions\InvalidAuthorException $ex){
http_response_code(404); Template::Emit404();
include(WEB_ROOT . '/404.php');
exit();
} }
?><?= 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)]) ?> ?><?= 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"> <main class="ebooks">

View file

@ -8,6 +8,13 @@ use function Safe\preg_replace;
use function Safe\apcu_fetch; use function Safe\apcu_fetch;
use function Safe\shuffle; use function Safe\shuffle;
$ebook = new Ebook();
$transcriptionSources = [];
$scanSources = [];
$otherSources = [];
$carousel = [];
$carouselTag = null;
try{ try{
$urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/ $urlPath = trim(str_replace('.', '', HttpInput::Str(GET, 'url-path', true) ?? ''), '/'); // Contains the portion of the URL (without query string) that comes after https://standardebooks.org/ebooks/
$wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook $wwwFilesystemPath = EBOOKS_DIST_PATH . $urlPath; // Path to the deployed WWW files for this ebook
@ -42,9 +49,6 @@ try{
} }
// Divide our sources into transcriptions and scans // Divide our sources into transcriptions and scans
$transcriptionSources = [];
$scanSources = [];
$otherSources = [];
foreach($ebook->Sources as $source){ foreach($ebook->Sources as $source){
switch($source->Type){ switch($source->Type){
case SOURCE_PROJECT_GUTENBERG: case SOURCE_PROJECT_GUTENBERG:
@ -69,9 +73,7 @@ try{
// Generate the bottom carousel. // Generate the bottom carousel.
// Pick a random tag from this ebook, and get ebooks in the same tag // Pick a random tag from this ebook, and get ebooks in the same tag
$carousel = [];
$ebooks = []; $ebooks = [];
$carouselTag = null;
if(sizeof($ebook->Tags) > 0){ if(sizeof($ebook->Tags) > 0){
$carouselTag = $ebook->Tags[rand(0, sizeof($ebook->Tags) - 1)]; $carouselTag = $ebook->Tags[rand(0, sizeof($ebook->Tags) - 1)];
$ebooks = Library::GetEbooksByTag(strtolower($carouselTag->Name)); $ebooks = Library::GetEbooksByTag(strtolower($carouselTag->Name));
@ -104,9 +106,7 @@ catch(Exceptions\SeeOtherEbookException $ex){
exit(); exit();
} }
catch(Exceptions\InvalidEbookException $ex){ catch(Exceptions\InvalidEbookException $ex){
http_response_code(404); Template::Emit404();
include(WEB_ROOT . '/404.php');
exit();
} }
?><?= 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]) ?> ?><?= 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> <main>

View file

@ -14,6 +14,10 @@ try{
$pages = 0; $pages = 0;
$totalEbooks = 0; $totalEbooks = 0;
$collectionObject = null; $collectionObject = null;
$pageDescription = '';
$pageTitle = '';
$pageHeader = '';
$queryString = '';
if($page <= 0){ if($page <= 0){
$page = 1; $page = 1;
@ -90,9 +94,8 @@ try{
if($page > 1){ if($page > 1){
$pageTitle .= ', page ' . $page; $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($collection === null){
if($query != ''){ if($query != ''){
@ -119,9 +122,7 @@ try{
$queryString = preg_replace('/^&amp;/ius', '', $queryString); $queryString = preg_replace('/^&amp;/ius', '', $queryString);
} }
catch(Exceptions\InvalidCollectionException $ex){ catch(Exceptions\InvalidCollectionException $ex){
http_response_code(404); Template::Emit404();
include(WEB_ROOT . '/404.php');
exit();
} }
?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?> ?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?>
<main class="ebooks"> <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(); throw new Exceptions\InvalidRequestException();
} }
$subscriber = NewsletterSubscriber::Get(HttpInput::Str(GET, 'uuid') ?? ''); $subscription = NewsletterSubscription::Get(HttpInput::Str(GET, 'uuid') ?? '');
$subscriber->Delete(); $subscription->Delete();
if($requestType == REST){ if($requestType == REST){
exit(); exit();
@ -22,13 +22,15 @@ catch(Exceptions\InvalidRequestException $ex){
http_response_code(405); http_response_code(405);
exit(); exit();
} }
catch(Exceptions\InvalidNewsletterSubscriberException $ex){ catch(Exceptions\InvalidNewsletterSubscriptionException $ex){
http_response_code(404);
if($requestType == WEB){ 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.']) ?> ?><?= Template::Header(['title' => 'Youve unsubscribed from the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'Youve unsubscribed from the Standard Ebooks newsletter.']) ?>
<main> <main>

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(); session_start();
$subscriber = $_SESSION['subscriber'] ?? new NewsletterSubscriber(); $subscription = $_SESSION['subscription'] ?? new NewsletterSubscription();
$exception = $_SESSION['exception'] ?? null; $exception = $_SESSION['exception'] ?? null;
if($exception){ if($exception){
@ -29,12 +29,12 @@ if($exception){
<?= Template::Error(['exception' => $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 */ ?> <label class="automation-test"><? /* Test for spam bots filling out all fields */ ?>
<input type="text" name="automationtest" value="" maxlength="80" /> <input type="text" name="automationtest" value="" maxlength="80" />
</label> </label>
<label class="email">Your email address <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>
<label class="captcha"> <label class="captcha">
Type the letters in the <abbr class="acronym">CAPTCHA</abbr> image 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> <p>What kind of email would you like to receive?</p>
<ul> <ul>
<li> <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>
<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> </li>
</ul> </ul>
</fieldset> </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; use Safe\DateTime;
$poll = null; $poll = new Poll();
try{ try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false)); $poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
} }
catch(Exceptions\SeException $ex){ catch(Exceptions\SeException $ex){
http_response_code(404); Template::Emit404();
include(WEB_ROOT . '/404.php');
exit();
} }
?><?= Template::Header(['title' => $poll->Name, 'highlight' => '', 'description' => $poll->Description]) ?> ?><?= Template::Header(['title' => $poll->Name, 'highlight' => '', 'description' => $poll->Description]) ?>

View file

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

View file

@ -1,15 +1,13 @@
<? <?
require_once('Core.php'); require_once('Core.php');
$poll = null; $poll = new Poll();
try{ try{
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false)); $poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
} }
catch(Exceptions\SeException $ex){ catch(Exceptions\SeException $ex){
http_response_code(404); Template::Emit404();
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!']) ?> ?><?= 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 // 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 ns.* from NewsletterSubscriptions ns inner join Users u on ns.UserId = u.UserId where u.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 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 // 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();