diff --git a/config/apache/standardebooks.org.conf b/config/apache/standardebooks.org.conf index 2c9241ee..ea6aa72e 100644 --- a/config/apache/standardebooks.org.conf +++ b/config/apache/standardebooks.org.conf @@ -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] diff --git a/config/apache/standardebooks.test.conf b/config/apache/standardebooks.test.conf index dc073862..6dfa2417 100644 --- a/config/apache/standardebooks.test.conf +++ b/config/apache/standardebooks.test.conf @@ -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] diff --git a/config/sql/se/NewsletterSubscribers.sql b/config/sql/se/NewsletterSubscribers.sql deleted file mode 100644 index 57446862..00000000 --- a/config/sql/se/NewsletterSubscribers.sql +++ /dev/null @@ -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; diff --git a/config/sql/se/NewsletterSubscriptions.sql b/config/sql/se/NewsletterSubscriptions.sql new file mode 100644 index 00000000..f7889f0b --- /dev/null +++ b/config/sql/se/NewsletterSubscriptions.sql @@ -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; diff --git a/lib/Db.php b/lib/Db.php index d879b088..c2f63aa3 100644 --- a/lib/Db.php +++ b/lib/Db.php @@ -5,6 +5,10 @@ class Db{ return $GLOBALS['DbConnection']->GetLastInsertedId(); } + public static function GetAffectedRowCount(): int{ + return $GLOBALS['DbConnection']->LastQueryAffectedRowCount; + } + /** * @param string $query * @param array $args diff --git a/lib/DbConnection.php b/lib/DbConnection.php index 9f5e9f53..26c96b5a 100644 --- a/lib/DbConnection.php +++ b/lib/DbConnection.php @@ -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{ diff --git a/lib/Ebook.php b/lib/Ebook.php index b9a6f61d..1b3d0183 100644 --- a/lib/Ebook.php +++ b/lib/Ebook.php @@ -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'; diff --git a/lib/Exceptions/InvalidNewsletterSubscriberException.php b/lib/Exceptions/InvalidNewsletterSubscriptionException.php similarity index 62% rename from lib/Exceptions/InvalidNewsletterSubscriberException.php rename to lib/Exceptions/InvalidNewsletterSubscriptionException.php index d23dc7ef..5a5a6909 100644 --- a/lib/Exceptions/InvalidNewsletterSubscriberException.php +++ b/lib/Exceptions/InvalidNewsletterSubscriptionException.php @@ -1,6 +1,6 @@ _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]; - } -} diff --git a/lib/NewsletterSubscription.php b/lib/NewsletterSubscription.php new file mode 100644 index 00000000..46e75094 --- /dev/null +++ b/lib/NewsletterSubscription.php @@ -0,0 +1,123 @@ +_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]; + } +} diff --git a/lib/Template.php b/lib/Template.php index 5216b9ae..79261146 100644 --- a/lib/Template.php +++ b/lib/Template.php @@ -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(); + } } diff --git a/scripts/delete-unconfirmed-newsletter-subscribers b/scripts/delete-unconfirmed-newsletter-subscribers index b9a4e30e..e008a2c7 100644 --- a/scripts/delete-unconfirmed-newsletter-subscribers +++ b/scripts/delete-unconfirmed-newsletter-subscribers @@ -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'); ?> diff --git a/templates/EmailHeader.php b/templates/EmailHeader.php index 0364bafd..c274a8f1 100644 --- a/templates/EmailHeader.php +++ b/templates/EmailHeader.php @@ -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; } diff --git a/templates/EmailNewsletterConfirmation.php b/templates/EmailNewsletterConfirmation.php index 6020092b..40538dfe 100644 --- a/templates/EmailNewsletterConfirmation.php +++ b/templates/EmailNewsletterConfirmation.php @@ -8,7 +8,7 @@

Please use the button below to confirm your subscription—you won’t receive email from us until you do.

- Yes, confirm my subscription + Yes, confirm my subscription

If you didn’t subscribe, or you’re not sure why you received this email, you can safely delete it and you won’t receive any more email from us.

diff --git a/templates/EmailNewsletterConfirmationText.php b/templates/EmailNewsletterConfirmationText.php index efc8e67a..2a1440b1 100644 --- a/templates/EmailNewsletterConfirmationText.php +++ b/templates/EmailNewsletterConfirmationText.php @@ -8,9 +8,10 @@ You subscribed to: - A monthly summary of new ebook releases + Please use the link below to confirm your subscription—you won’t receive email from us until you do. -<Url ?>/confirm> +<Url ?>/confirm> If you didn’t subscribe, or you’re not sure why you received this email, you can safely delete it and you won’t receive any more email from us. diff --git a/templates/Error.php b/templates/Error.php index 38b0d099..97e52ee0 100644 --- a/templates/Error.php +++ b/templates/Error.php @@ -13,7 +13,7 @@ else{ $exceptions[] = $exception; } ?> -