mirror of
https://github.com/standardebooks/web.git
synced 2025-07-05 22:30:30 -04:00
Add newsletter management functionality
This commit is contained in:
parent
90ee0a93c9
commit
b0197d189a
57 changed files with 1017 additions and 143 deletions
|
@ -6,7 +6,7 @@ PHP 7+ is required.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Install Apache, PHP, PHP-FPM, and various other dependencies.
|
# Install Apache, PHP, PHP-FPM, and various other dependencies.
|
||||||
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc
|
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server
|
||||||
|
|
||||||
# Create the site root and logs root and clone this repo into it.
|
# Create the site root and logs root and clone this repo into it.
|
||||||
sudo mkdir /standardebooks.org/
|
sudo mkdir /standardebooks.org/
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
"files": ["lib/Constants.php", "lib/CoreFunctions.php"]
|
"files": ["lib/Constants.php", "lib/CoreFunctions.php"]
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"thecodingmachine/safe": "^1.0.0"
|
"thecodingmachine/safe": "^1.0.0",
|
||||||
|
"phpmailer/phpmailer": "6.6.0",
|
||||||
|
"ramsey/uuid": "4.2.3",
|
||||||
|
"gregwar/captcha": "1.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,10 +142,9 @@ Define webroot /standardebooks.org/web
|
||||||
# In RewriteCond, RewriteRule gets evaluated BEFORE RewriteCond, so $1 refers to the first
|
# In RewriteCond, RewriteRule gets evaluated BEFORE RewriteCond, so $1 refers to the first
|
||||||
# match in RewriteRule
|
# match in RewriteRule
|
||||||
# Rewrite POST /some/url -> POST /some/url/post.php
|
# Rewrite POST /some/url -> POST /some/url/post.php
|
||||||
RewriteCond %{REQUEST_METHOD} ^POST$
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^(post|delete|put)$/"
|
||||||
RewriteCond %{DOCUMENT_ROOT}/$1/ -d
|
RewriteCond %{DOCUMENT_ROOT}/$1/%1.php -f
|
||||||
RewriteCond %{DOCUMENT_ROOT}/$1/post.php -f
|
RewriteRule ^([^\.]+)$ $1/%1.php [L]
|
||||||
RewriteRule ^([^\.]+)$ $1/post.php [L]
|
|
||||||
|
|
||||||
# In case of 404, serve the 404 page specified by ErrorDocument, not the default FPM error page.
|
# In case of 404, serve the 404 page specified by ErrorDocument, not the default FPM error page.
|
||||||
# Note that we can't use `ProxyErrorOverride on` because that catches ALL 4xx and 5xx HTTP headers
|
# Note that we can't use `ProxyErrorOverride on` because that catches ALL 4xx and 5xx HTTP headers
|
||||||
|
@ -243,6 +242,10 @@ Define webroot /standardebooks.org/web
|
||||||
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
|
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
|
||||||
RewriteCond %{QUERY_STRING} ^query=
|
RewriteCond %{QUERY_STRING} ^query=
|
||||||
RewriteRule ^/opds/all.xml$ /opds/search.php [QSA]
|
RewriteRule ^/opds/all.xml$ /opds/search.php [QSA]
|
||||||
|
|
||||||
|
# Newsletter
|
||||||
|
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
|
||||||
|
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
|
|
@ -141,10 +141,9 @@ Define webroot /standardebooks.org/web
|
||||||
# In RewriteCond, RewriteRule gets evaluated BEFORE RewriteCond, so $1 refers to the first
|
# In RewriteCond, RewriteRule gets evaluated BEFORE RewriteCond, so $1 refers to the first
|
||||||
# match in RewriteRule
|
# match in RewriteRule
|
||||||
# Rewrite POST /some/url -> POST /some/url/post.php
|
# Rewrite POST /some/url -> POST /some/url/post.php
|
||||||
RewriteCond %{REQUEST_METHOD} ^POST$
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^(post|delete|put)$/"
|
||||||
RewriteCond %{DOCUMENT_ROOT}/$1/ -d
|
RewriteCond %{DOCUMENT_ROOT}/$1/%1.php -f
|
||||||
RewriteCond %{DOCUMENT_ROOT}/$1/post.php -f
|
RewriteRule ^([^\.]+)$ $1/%1.php [L]
|
||||||
RewriteRule ^([^\.]+)$ $1/post.php [L]
|
|
||||||
|
|
||||||
# In case of 404, serve the 404 page specified by ErrorDocument, not the default FPM error page.
|
# In case of 404, serve the 404 page specified by ErrorDocument, not the default FPM error page.
|
||||||
# Note that we can't use `ProxyErrorOverride on` because that catches ALL 4xx and 5xx HTTP headers
|
# Note that we can't use `ProxyErrorOverride on` because that catches ALL 4xx and 5xx HTTP headers
|
||||||
|
@ -242,4 +241,8 @@ Define webroot /standardebooks.org/web
|
||||||
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
|
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
|
||||||
RewriteCond %{QUERY_STRING} ^query=
|
RewriteCond %{QUERY_STRING} ^query=
|
||||||
RewriteRule ^/opds/all.xml$ /opds/search.php [QSA]
|
RewriteRule ^/opds/all.xml$ /opds/search.php [QSA]
|
||||||
|
|
||||||
|
# Newsletter
|
||||||
|
RewriteRule ^/newsletter$ /newsletter/subscribers/new.php
|
||||||
|
RewriteRule ^/newsletter/subscribers/([^\./]+?)/(delete|confirm)$ /newsletter/subscribers/$2.php?uuid=$1
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
14
config/sql/se/NewsletterSubscribers.sql
Normal file
14
config/sql/se/NewsletterSubscribers.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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,
|
||||||
|
`Timestamp` datetime NOT NULL,
|
||||||
|
PRIMARY KEY (`NewsletterSubscriberId`),
|
||||||
|
UNIQUE KEY `Uuid_UNIQUE` (`Uuid`),
|
||||||
|
UNIQUE KEY `Email_UNIQUE` (`Email`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
@ -1,12 +1,29 @@
|
||||||
<?
|
<?
|
||||||
// Auto-included by Composer in composer.json to satisfy PHPStan
|
// Auto-included by Composer in composer.json to satisfy PHPStan
|
||||||
use function Safe\define;
|
use function Safe\define;
|
||||||
|
use function Safe\file_get_contents;
|
||||||
|
use function Safe\gmdate;
|
||||||
use function Safe\strtotime;
|
use function Safe\strtotime;
|
||||||
|
|
||||||
const SITE_STATUS_LIVE = 'live';
|
const SITE_STATUS_LIVE = 'live';
|
||||||
const SITE_STATUS_DEV = 'dev';
|
const SITE_STATUS_DEV = 'dev';
|
||||||
define('SITE_STATUS', getenv('SITE_STATUS') ?: SITE_STATUS_DEV); // Set in the PHP FPM pool configuration. Have to use define() and not const so we can use a function.
|
define('SITE_STATUS', getenv('SITE_STATUS') ?: SITE_STATUS_DEV); // Set in the PHP FPM pool configuration. Have to use define() and not const so we can use a function.
|
||||||
|
|
||||||
|
// No trailing slash on any of the below constants.
|
||||||
|
if(SITE_STATUS == SITE_STATUS_LIVE){
|
||||||
|
define('SITE_URL', 'https://standardebooks.org');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
define('SITE_URL', 'https://standardebooks.test');
|
||||||
|
}
|
||||||
|
|
||||||
|
const SITE_ROOT = '/standardebooks.org';
|
||||||
|
const WEB_ROOT = SITE_ROOT . '/web/www';
|
||||||
|
const REPOS_PATH = SITE_ROOT . '/ebooks';
|
||||||
|
const TEMPLATES_PATH = SITE_ROOT . '/web/templates';
|
||||||
|
const MANUAL_PATH = WEB_ROOT . '/manual';
|
||||||
|
const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/';
|
||||||
|
|
||||||
const DATABASE_DEFAULT_DATABASE = 'se';
|
const DATABASE_DEFAULT_DATABASE = 'se';
|
||||||
const DATABASE_DEFAULT_HOST = 'localhost';
|
const DATABASE_DEFAULT_HOST = 'localhost';
|
||||||
|
|
||||||
|
@ -16,9 +33,21 @@ const SORT_AUTHOR_ALPHA = 'author-alpha';
|
||||||
const SORT_READING_EASE = 'reading-ease';
|
const SORT_READING_EASE = 'reading-ease';
|
||||||
const SORT_LENGTH = 'length';
|
const SORT_LENGTH = 'length';
|
||||||
|
|
||||||
const GET = 0;
|
const CAPTCHA_IMAGE_HEIGHT = 72;
|
||||||
const POST = 1;
|
const CAPTCHA_IMAGE_WIDTH = 230;
|
||||||
const COOKIE = 2;
|
|
||||||
|
const NO_REPLY_EMAIL_ADDRESS = 'admin@standardebooks.org';
|
||||||
|
const EMAIL_SMTP_HOST = 'smtp-broadcasts.postmarkapp.com';
|
||||||
|
define('EMAIL_SMTP_USERNAME', trim(file_get_contents(SITE_ROOT . '/config/secrets/postmarkapp.com')) ?: '');
|
||||||
|
const EMAIL_SMTP_PASSWORD = EMAIL_SMTP_USERNAME;
|
||||||
|
const EMAIL_POSTMARK_STREAM_BROADCAST = 'the-standard-ebooks-newsletter';
|
||||||
|
|
||||||
|
const REST = 0;
|
||||||
|
const WEB = 1;
|
||||||
|
|
||||||
|
const GET = 'GET';
|
||||||
|
const POST = 'POST';
|
||||||
|
const COOKIE = 'COOKIE';
|
||||||
|
|
||||||
const HTTP_VAR_INT = 0;
|
const HTTP_VAR_INT = 0;
|
||||||
const HTTP_VAR_STR = 1;
|
const HTTP_VAR_STR = 1;
|
||||||
|
@ -47,17 +76,8 @@ define('DONATION_HOLIDAY_ALERT_ON', time() > strtotime('November 15, ' . gmdate(
|
||||||
define('DONATION_ALERT_ON', DONATION_HOLIDAY_ALERT_ON || rand(1, 4) == 2);
|
define('DONATION_ALERT_ON', DONATION_HOLIDAY_ALERT_ON || rand(1, 4) == 2);
|
||||||
define('DONATION_DRIVE_ON', false);
|
define('DONATION_DRIVE_ON', false);
|
||||||
|
|
||||||
// No trailing slash on any of the below constants.
|
|
||||||
const SITE_URL = 'https://standardebooks.org';
|
|
||||||
const SITE_ROOT = '/standardebooks.org';
|
|
||||||
const WEB_ROOT = SITE_ROOT . '/web/www';
|
|
||||||
const REPOS_PATH = SITE_ROOT . '/ebooks';
|
|
||||||
const TEMPLATES_PATH = SITE_ROOT . '/web/templates';
|
|
||||||
const MANUAL_PATH = WEB_ROOT . '/manual';
|
|
||||||
const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/';
|
|
||||||
|
|
||||||
const GITHUB_SECRET_FILE_PATH = SITE_ROOT . '/config/secrets/se-vcs-bot@github.com'; // Set in the GitHub organization global webhook settings.
|
const GITHUB_SECRET_FILE_PATH = SITE_ROOT . '/config/secrets/se-vcs-bot@github.com'; // Set in the GitHub organization global webhook settings.
|
||||||
const GITHUB_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-github.log'; // Must be writable by `www-data` Unix user.
|
const GITHUB_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-github.log'; // Must be writable by `www-data` Unix user.
|
||||||
|
const GITHUB_IGNORED_REPOS = ['tools', 'manual', 'web']; // If we get GitHub push requests featuring these repos, silently ignore instead of returning an error.
|
||||||
|
|
||||||
// If we get GitHub push requests featuring these repos, silently ignore instead of returning an error.
|
const POSTMARK_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-postmark.log'; // Must be writable by `www-data` Unix user.
|
||||||
const GITHUB_IGNORED_REPOS = ['tools', 'manual', 'web'];
|
|
||||||
|
|
|
@ -10,7 +10,10 @@ class DbConnection{
|
||||||
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){
|
||||||
// Get the user running the script for local socket login
|
// Get the user running the script for local socket login
|
||||||
$user = posix_getpwuid(posix_geteuid())['name'];
|
$user = posix_getpwuid(posix_geteuid());
|
||||||
|
if($user){
|
||||||
|
$user = $user['name'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$connectionString = 'mysql:';
|
$connectionString = 'mysql:';
|
||||||
|
@ -125,9 +128,9 @@ class DbConnection{
|
||||||
|
|
||||||
usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds
|
usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds
|
||||||
}
|
}
|
||||||
elseif(stripos($ex->getMessage(), '1064 offset out of bounds') !== false){
|
elseif($ex->getCode() == '23000'){
|
||||||
$done = true;
|
// Duplicate key, bubble this up without logging it so the business logic can handle it
|
||||||
// We reach here if Sphinx tries to get a record past its page limit. Just silently do nothing.
|
throw($ex);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
$done = true;
|
$done = true;
|
||||||
|
@ -139,16 +142,12 @@ class DbConnection{
|
||||||
Logger::WriteErrorLogEntry($ex->getMessage());
|
Logger::WriteErrorLogEntry($ex->getMessage());
|
||||||
Logger::WriteErrorLogEntry($preparedSql);
|
Logger::WriteErrorLogEntry($preparedSql);
|
||||||
Logger::WriteErrorLogEntry(vds($params));
|
Logger::WriteErrorLogEntry(vds($params));
|
||||||
|
throw($ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only one rowset is returned, change the result object
|
|
||||||
if(sizeof($result) == 1){
|
|
||||||
$result = $result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,7 +167,7 @@ class DbConnection{
|
||||||
|
|
||||||
for($i = 0; $i < $columnCount; $i++){
|
for($i = 0; $i < $columnCount; $i++){
|
||||||
$metadata[$i] = $handle->getColumnMeta($i);
|
$metadata[$i] = $handle->getColumnMeta($i);
|
||||||
if(preg_match('/^(Is|Has|Can)[A-Z]/u', $metadata[$i]['name']) === 1){
|
if($metadata[$i] && preg_match('/^(Is|Has|Can)[A-Z]/u', $metadata[$i]['name']) === 1){
|
||||||
// MySQL doesn't have a native boolean type, so fake it here if the column
|
// MySQL doesn't have a native boolean type, so fake it here if the column
|
||||||
// name starts with Is, Has, or Can and is followed by an uppercase letter
|
// name starts with Is, Has, or Can and is followed by an uppercase letter
|
||||||
$metadata[$i]['native_type'] = 'BOOL';
|
$metadata[$i]['native_type'] = 'BOOL';
|
||||||
|
|
|
@ -69,15 +69,15 @@ class Ebook{
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!is_dir($wwwFilesystemPath)){
|
if(!is_dir($wwwFilesystemPath)){
|
||||||
throw new InvalidEbookException('Invalid www filesystem path: ' . $wwwFilesystemPath);
|
throw new Exceptions\InvalidEbookException('Invalid www filesystem path: ' . $wwwFilesystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!is_dir($this->RepoFilesystemPath)){
|
if(!is_dir($this->RepoFilesystemPath)){
|
||||||
throw new InvalidEbookException('Invalid repo filesystem path: ' . $this->RepoFilesystemPath);
|
throw new Exceptions\InvalidEbookException('Invalid repo filesystem path: ' . $this->RepoFilesystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!is_file($wwwFilesystemPath . '/content.opf')){
|
if(!is_file($wwwFilesystemPath . '/content.opf')){
|
||||||
throw new InvalidEbookException('Invalid content.opf file: ' . $wwwFilesystemPath . '/content.opf');
|
throw new Exceptions\InvalidEbookException('Invalid content.opf file: ' . $wwwFilesystemPath . '/content.opf');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->WwwFilesystemPath = $wwwFilesystemPath;
|
$this->WwwFilesystemPath = $wwwFilesystemPath;
|
||||||
|
@ -88,7 +88,7 @@ class Ebook{
|
||||||
// Get the SE identifier.
|
// Get the SE identifier.
|
||||||
preg_match('|<dc:identifier[^>]*?>(.+?)</dc:identifier>|ius', $rawMetadata, $matches);
|
preg_match('|<dc:identifier[^>]*?>(.+?)</dc:identifier>|ius', $rawMetadata, $matches);
|
||||||
if(sizeof($matches) != 2){
|
if(sizeof($matches) != 2){
|
||||||
throw new EbookParsingException('Invalid <dc:identifier> element.');
|
throw new Exceptions\EbookParsingException('Invalid <dc:identifier> element.');
|
||||||
}
|
}
|
||||||
$this->Identifier = (string)$matches[1];
|
$this->Identifier = (string)$matches[1];
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ class Ebook{
|
||||||
|
|
||||||
$this->Title = $this->NullIfEmpty($xml->xpath('/package/metadata/dc:title'));
|
$this->Title = $this->NullIfEmpty($xml->xpath('/package/metadata/dc:title'));
|
||||||
if($this->Title === null){
|
if($this->Title === null){
|
||||||
throw new EbookParsingException('Invalid <dc:title> element.');
|
throw new Exceptions\EbookParsingException('Invalid <dc:title> element.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->Title = str_replace('\'', '’', $this->Title);
|
$this->Title = str_replace('\'', '’', $this->Title);
|
||||||
|
@ -256,7 +256,7 @@ class Ebook{
|
||||||
}
|
}
|
||||||
|
|
||||||
if(sizeof($this->Authors) == 0){
|
if(sizeof($this->Authors) == 0){
|
||||||
throw new EbookParsingException('Invalid <dc:creator> element.');
|
throw new Exceptions\EbookParsingException('Invalid <dc:creator> element.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->AuthorsUrl = preg_replace('|url:https://standardebooks.org/ebooks/([^/]+)/.*|ius', '/ebooks/\1', $this->Identifier);
|
$this->AuthorsUrl = preg_replace('|url:https://standardebooks.org/ebooks/([^/]+)/.*|ius', '/ebooks/\1', $this->Identifier);
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<?
|
|
||||||
class EbookParsingException extends \Exception{
|
|
||||||
}
|
|
88
lib/Email.php
Normal file
88
lib/Email.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
class Email{
|
||||||
|
public $To = '';
|
||||||
|
public $From = '';
|
||||||
|
public $FromName = '';
|
||||||
|
public $ReplyTo = '';
|
||||||
|
public $Subject = '';
|
||||||
|
public $Body = '';
|
||||||
|
public $TextBody = '';
|
||||||
|
public $Attachments = array();
|
||||||
|
public $PostmarkStream = null;
|
||||||
|
|
||||||
|
public function Send(): bool{
|
||||||
|
if($this->ReplyTo == ''){
|
||||||
|
$this->ReplyTo = $this->From;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->To === null || $this->To == ''){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phpMailer = new PHPMailer(true);
|
||||||
|
|
||||||
|
try{
|
||||||
|
$phpMailer->SetFrom($this->From, $this->FromName);
|
||||||
|
$phpMailer->AddReplyTo($this->ReplyTo);
|
||||||
|
$phpMailer->AddAddress($this->To);
|
||||||
|
$phpMailer->Subject = $this->Subject;
|
||||||
|
$phpMailer->CharSet = 'UTF-8';
|
||||||
|
if($this->TextBody !== null && $this->TextBody != ''){
|
||||||
|
$phpMailer->IsHTML(true);
|
||||||
|
$phpMailer->Body = $this->Body;
|
||||||
|
$phpMailer->AltBody = $this->TextBody;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$phpMailer->MsgHTML($this->Body);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->Attachments as $attachment){
|
||||||
|
if(is_array($attachment)){
|
||||||
|
$phpMailer->addStringAttachment($attachment['contents'], $attachment['filename']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$phpMailer->IsSMTP();
|
||||||
|
$phpMailer->SMTPAuth = true;
|
||||||
|
$phpMailer->SMTPSecure = 'tls';
|
||||||
|
$phpMailer->Port = 587;
|
||||||
|
$phpMailer->Host = EMAIL_SMTP_HOST;
|
||||||
|
$phpMailer->Username = EMAIL_SMTP_USERNAME;
|
||||||
|
$phpMailer->Password = EMAIL_SMTP_PASSWORD;
|
||||||
|
|
||||||
|
if($this->PostmarkStream !== null){
|
||||||
|
$phpMailer->addCustomHeader('X-PM-Message-Stream', $this->PostmarkStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(SITE_STATUS == SITE_STATUS_DEV){
|
||||||
|
Logger::WriteErrorLogEntry('Sending mail to ' . $this->To . ' from ' . $this->From);
|
||||||
|
Logger::WriteErrorLogEntry('Subject: ' . $this->Subject);
|
||||||
|
Logger::WriteErrorLogEntry($this->Body);
|
||||||
|
Logger::WriteErrorLogEntry($this->TextBody);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$phpMailer->Send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $ex){
|
||||||
|
if(SITE_STATUS != SITE_STATUS_DEV){
|
||||||
|
Logger::WriteErrorLogEntry('Failed sending email to ' . $this->To . ' Exception: ' . $ex->errorMessage() . "\n" . ' Subject: ' . $this->Subject . "\nBody:\n" . $this->Body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(bool $isNoReplyEmail = false){
|
||||||
|
if($isNoReplyEmail){
|
||||||
|
$this->From = NO_REPLY_EMAIL_ADDRESS;
|
||||||
|
$this->FromName = 'Standard Ebooks';
|
||||||
|
$this->ReplyTo = NO_REPLY_EMAIL_ADDRESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
lib/Exceptions/EbookParsingException.php
Normal file
5
lib/Exceptions/EbookParsingException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class EbookParsingException extends SeException{
|
||||||
|
}
|
5
lib/Exceptions/InvalidAuthorException.php
Normal file
5
lib/Exceptions/InvalidAuthorException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidAuthorException extends SeException{
|
||||||
|
}
|
6
lib/Exceptions/InvalidCaptchaException.php
Normal file
6
lib/Exceptions/InvalidCaptchaException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidCaptchaException extends SeException{
|
||||||
|
protected $message = 'We couldn’t validate your CAPTCHA response.';
|
||||||
|
}
|
5
lib/Exceptions/InvalidCollectionException.php
Normal file
5
lib/Exceptions/InvalidCollectionException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidCollectionException extends SeException{
|
||||||
|
}
|
6
lib/Exceptions/InvalidCredentialsException.php
Normal file
6
lib/Exceptions/InvalidCredentialsException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidCredentialsException extends SeException{
|
||||||
|
protected $message = 'Invalid credentials.';
|
||||||
|
}
|
5
lib/Exceptions/InvalidEbookException.php
Normal file
5
lib/Exceptions/InvalidEbookException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidEbookException extends SeException{
|
||||||
|
}
|
6
lib/Exceptions/InvalidEmailException.php
Normal file
6
lib/Exceptions/InvalidEmailException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidEmailException extends SeException{
|
||||||
|
protected $message = 'We couldn’t understand your email address.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidNewsletterSubscriberException.php
Normal file
6
lib/Exceptions/InvalidNewsletterSubscriberException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidNewsletterSubscriberException extends SeException{
|
||||||
|
protected $message = 'We couldn’t find you in our newsletter subscribers list.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidRequestException.php
Normal file
6
lib/Exceptions/InvalidRequestException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidRequestException extends SeException{
|
||||||
|
protected $message = 'Invalid request.';
|
||||||
|
}
|
6
lib/Exceptions/NewsletterRequiredException.php
Normal file
6
lib/Exceptions/NewsletterRequiredException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class NewsletterRequiredException extends SeException{
|
||||||
|
protected $message = 'You must select at least one newsletter to subscribe to.';
|
||||||
|
}
|
6
lib/Exceptions/NewsletterSubscriberExistsException.php
Normal file
6
lib/Exceptions/NewsletterSubscriberExistsException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class NewsletterSubscriberExistsException extends SeException{
|
||||||
|
protected $message = 'You’re already subscribed to the newsletter.';
|
||||||
|
}
|
5
lib/Exceptions/NoopException.php
Normal file
5
lib/Exceptions/NoopException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class NoopException extends SeException{
|
||||||
|
}
|
5
lib/Exceptions/SeException.php
Normal file
5
lib/Exceptions/SeException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class SeException extends \Exception{
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
<?
|
<?
|
||||||
class SeeOtherEbookException extends \Exception{
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class SeeOtherEbookException extends SeException{
|
||||||
public $Url;
|
public $Url;
|
||||||
|
|
||||||
public function __construct(string $url = ''){
|
public function __construct(string $url = ''){
|
63
lib/Exceptions/ValidationException.php
Normal file
63
lib/Exceptions/ValidationException.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
use function Safe\json_encode;
|
||||||
|
|
||||||
|
class ValidationException extends SeException{
|
||||||
|
public $Exceptions = [];
|
||||||
|
public $HasExceptions = false;
|
||||||
|
public $IsFatal = false;
|
||||||
|
|
||||||
|
public function __toString(): string{
|
||||||
|
$output = '';
|
||||||
|
foreach($this->Exceptions as $exception){
|
||||||
|
$output .= $exception->getMessage() . '; ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($output, '; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Add(\Exception $exception, bool $isFatal = false): void{
|
||||||
|
if(is_a($exception, static::class)){
|
||||||
|
foreach($exception->Exceptions as $childException){
|
||||||
|
$this->Add($childException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->Exceptions[] = $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($isFatal){
|
||||||
|
$this->IsFatal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->HasExceptions = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Serialize(): string{
|
||||||
|
$val = '';
|
||||||
|
foreach($this->Exceptions as $childException){
|
||||||
|
$val .= $childException->getCode() . ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = rtrim($val, ',');
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Has(string $exception): bool{
|
||||||
|
foreach($this->Exceptions as $childException){
|
||||||
|
if(is_a($childException, $exception)){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Clear(): void{
|
||||||
|
unset($this->Exceptions);
|
||||||
|
$this->Exceptions = [];
|
||||||
|
$this->HasExceptions = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
<?
|
<?
|
||||||
class WebhookException extends \Exception{
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class WebhookException extends SeException{
|
||||||
public $PostData;
|
public $PostData;
|
||||||
|
|
||||||
public function __construct(string $message = '', string $data = null){
|
public function __construct(string $message = '', string $data = null){
|
|
@ -32,7 +32,7 @@ class Formatter{
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function ToPlainText(string $text): string{
|
public static function ToPlainText(?string $text): string{
|
||||||
return htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8');
|
return htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?
|
<?
|
||||||
class HttpInput{
|
class HttpInput{
|
||||||
public static function Str(int $type, string $variable, bool $allowEmptyString = true, string $default = null): ?string{
|
public static function Str(string $type, string $variable, bool $allowEmptyString = true, string $default = null): ?string{
|
||||||
$var = self::GetHttpVar($variable, HTTP_VAR_STR, $type, $default);
|
$var = self::GetHttpVar($variable, HTTP_VAR_STR, $type, $default);
|
||||||
|
|
||||||
if(is_array($var)){
|
if(is_array($var)){
|
||||||
|
@ -14,15 +14,15 @@ class HttpInput{
|
||||||
return $var;
|
return $var;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function Int(int $type, string $variable, int $default = null): ?int{
|
public static function Int(string $type, string $variable, int $default = null): ?int{
|
||||||
return self::GetHttpVar($variable, HTTP_VAR_INT, $type, $default);
|
return self::GetHttpVar($variable, HTTP_VAR_INT, $type, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function Bool(int $type, string $variable, bool $default = null): ?bool{
|
public static function Bool(string $type, string $variable, bool $default = null): ?bool{
|
||||||
return self::GetHttpVar($variable, HTTP_VAR_BOOL, $type, $default);
|
return self::GetHttpVar($variable, HTTP_VAR_BOOL, $type, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function Dec(int $type, string $variable, float $default = null): ?float{
|
public static function Dec(string $type, string $variable, float $default = null): ?float{
|
||||||
return self::GetHttpVar($variable, HTTP_VAR_DEC, $type, $default);
|
return self::GetHttpVar($variable, HTTP_VAR_DEC, $type, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class HttpInput{
|
||||||
return self::GetHttpVar($variable, HTTP_VAR_ARRAY, GET, $default);
|
return self::GetHttpVar($variable, HTTP_VAR_ARRAY, GET, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function GetHttpVar(string $variable, int $type, int $set, $default){
|
private static function GetHttpVar(string $variable, int $type, string $set, $default){
|
||||||
$vars = [];
|
$vars = [];
|
||||||
|
|
||||||
switch($set){
|
switch($set){
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<?
|
|
||||||
class InvalidAuthorException extends \Exception{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<?
|
|
||||||
class InvalidCollectionException extends \Exception{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<?
|
|
||||||
class InvalidEbookException extends \Exception{
|
|
||||||
}
|
|
|
@ -3,18 +3,27 @@ use function Safe\fopen;
|
||||||
use function Safe\fwrite;
|
use function Safe\fwrite;
|
||||||
use function Safe\fclose;
|
use function Safe\fclose;
|
||||||
use function Safe\error_log;
|
use function Safe\error_log;
|
||||||
|
use function Safe\gmdate;
|
||||||
|
|
||||||
class Logger{
|
class Logger{
|
||||||
|
public static function WritePostmarkWebhookLogEntry(string $requestId, string $text): void{
|
||||||
|
self::WriteLogEntry(POSTMARK_WEBHOOK_LOG_FILE_PATH, $requestId . "\t" . $text);
|
||||||
|
}
|
||||||
|
|
||||||
public static function WriteGithubWebhookLogEntry(string $requestId, string $text): void{
|
public static function WriteGithubWebhookLogEntry(string $requestId, string $text): void{
|
||||||
|
self::WriteLogEntry(GITHUB_WEBHOOK_LOG_FILE_PATH, $requestId . "\t" . $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function WriteLogEntry(string $file, string $text): void{
|
||||||
try{
|
try{
|
||||||
$fp = fopen(GITHUB_WEBHOOK_LOG_FILE_PATH, 'a+');
|
$fp = fopen($file, 'a+');
|
||||||
}
|
}
|
||||||
catch(\Exception $ex){
|
catch(\Exception $ex){
|
||||||
self::WriteErrorLogEntry('Couldn\'t open log file: ' . GITHUB_WEBHOOK_LOG_FILE_PATH . '. Exception: ' . vds($ex));
|
self::WriteErrorLogEntry('Couldn\'t open log file: ' . $file . '. Exception: ' . vds($ex));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite($fp, gmdate('Y-m-d H:i:s') . "\t" . $requestId . "\t" . $text . "\n");
|
fwrite($fp, gmdate('Y-m-d H:i:s') . "\t" . $text . "\n");
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
89
lib/NewsletterSubscriber.php
Normal file
89
lib/NewsletterSubscriber.php
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
class NewsletterSubscriber extends PropertiesBase{
|
||||||
|
public $NewsletterSubscriberId;
|
||||||
|
public $Uuid;
|
||||||
|
public $Email;
|
||||||
|
public $FirstName;
|
||||||
|
public $LastName;
|
||||||
|
public $IsConfirmed = false;
|
||||||
|
public $IsSubscribedToSummary = true;
|
||||||
|
public $IsSubscribedToNewsletter = true;
|
||||||
|
public $Timestamp;
|
||||||
|
protected $Url = null;
|
||||||
|
|
||||||
|
protected function GetUrl(): string{
|
||||||
|
if($this->Url === null){
|
||||||
|
$this->Url = SITE_URL . '/newsletter/subscribers/' . $this->Uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Create(): void{
|
||||||
|
$this->Validate();
|
||||||
|
|
||||||
|
$uuid = Uuid::uuid4();
|
||||||
|
$this->Uuid = $uuid->toString();
|
||||||
|
|
||||||
|
try{
|
||||||
|
Db::Query('insert into NewsletterSubscribers (Email, Uuid, FirstName, LastName, IsConfirmed, IsSubscribedToNewsletter, IsSubscribedToSummary, Timestamp) values (?, ?, ?, ?, ?, ?, ?, utc_timestamp());', [$this->Email, $this->Uuid, $this->FirstName, $this->LastName, false, $this->IsSubscribedToNewsletter, $this->IsSubscribedToSummary]);
|
||||||
|
}
|
||||||
|
catch(PDOException $ex){
|
||||||
|
if($ex->getCode() == '23000'){
|
||||||
|
// Duplicate unique key; email already in use
|
||||||
|
throw new Exceptions\NewsletterSubscriberExistsException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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]);
|
||||||
|
$em->TextBody = Template::EmailNewsletterConfirmationText(['subscriber' => $this]);
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function Get(string $uuid): NewsletterSubscriber{
|
||||||
|
if($uuid == ''){
|
||||||
|
throw new Exceptions\InvalidNewsletterSubscriberException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscribers = Db::Query('select * from NewsletterSubscribers where Uuid = ?;', [$uuid], 'NewsletterSubscriber');
|
||||||
|
|
||||||
|
if(sizeof($subscribers) == 0){
|
||||||
|
throw new Exceptions\InvalidNewsletterSubscriberException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $subscribers[0];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<?
|
|
||||||
class NoopException extends \Exception{
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?
|
<?
|
||||||
use function Safe\file_get_contents;
|
use function Safe\file_get_contents;
|
||||||
use function Safe\file_put_contents;
|
use function Safe\file_put_contents;
|
||||||
|
use function Safe\gmdate;
|
||||||
use function Safe\rename;
|
use function Safe\rename;
|
||||||
use function Safe\tempnam;
|
use function Safe\tempnam;
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class OpdsFeed{
|
||||||
$xml->registerXPathNamespace('schema', 'http://schema.org/');
|
$xml->registerXPathNamespace('schema', 'http://schema.org/');
|
||||||
$entries = $xml->xpath('/feed/entry');
|
$entries = $xml->xpath('/feed/entry');
|
||||||
|
|
||||||
if($entries === false){
|
if(!$entries){
|
||||||
$entries = [];
|
$entries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class OpdsFeed{
|
||||||
// while updating it at the same time.
|
// while updating it at the same time.
|
||||||
$elements = $xml->xpath('/feed/entry/updated');
|
$elements = $xml->xpath('/feed/entry/updated');
|
||||||
|
|
||||||
if($elements === false){
|
if(!$elements){
|
||||||
$elements = [];
|
$elements = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class OpdsFeed{
|
||||||
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($parentFilepath)));
|
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($parentFilepath)));
|
||||||
|
|
||||||
$feedEntries = $xml->xpath('/feed/entry[id="' . $this->Id . '"]/updated');
|
$feedEntries = $xml->xpath('/feed/entry[id="' . $this->Id . '"]/updated');
|
||||||
if($feedEntries === false){
|
if(!$feedEntries){
|
||||||
$feedEntries = [];
|
$feedEntries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?
|
<?
|
||||||
use function Safe\file_get_contents;
|
use function Safe\file_get_contents;
|
||||||
use function Safe\file_put_contents;
|
use function Safe\file_put_contents;
|
||||||
|
use function Safe\gmdate;
|
||||||
use function Safe\rename;
|
use function Safe\rename;
|
||||||
use function Safe\tempnam;
|
use function Safe\tempnam;
|
||||||
|
|
||||||
|
|
6
scripts/delete-unconfirmed-newsletter-subscribers.php
Normal file
6
scripts/delete-unconfirmed-newsletter-subscribers.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
require_once('/standardebooks.org/web/lib/Core.php');
|
||||||
|
|
||||||
|
// Delete unconfirmed newsletter subscribers who are more than a week old
|
||||||
|
Db::Query('delete from NewsletterSubscribers where IsConfirmed = false and datediff(utc_timestamp(), Timestamp) >= 7');
|
||||||
|
?>
|
|
@ -3,6 +3,7 @@ require_once('/standardebooks.org/web/lib/Core.php');
|
||||||
|
|
||||||
use function Safe\file_get_contents;
|
use function Safe\file_get_contents;
|
||||||
use function Safe\getopt;
|
use function Safe\getopt;
|
||||||
|
use function Safe\gmdate;
|
||||||
use function Safe\krsort;
|
use function Safe\krsort;
|
||||||
use function Safe\preg_replace;
|
use function Safe\preg_replace;
|
||||||
use function Safe\strtotime;
|
use function Safe\strtotime;
|
||||||
|
|
12
templates/EmailFooter.php
Normal file
12
templates/EmailFooter.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="footer">
|
||||||
|
<p>
|
||||||
|
<a href="https://standardebooks.org/donate">Donate</a> • <a href="https://standardebooks.org/contribute">Get involved</a> • <a href="https://standardebooks.org/opds">OPDS</a> • <a href="https://standardebooks.org/rss/new-releases">New releases RSS feed</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://standardebooks.org">
|
||||||
|
<img src="https://standardebooks.org/images/logo-small.png" alt="The Standard Ebooks logo"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
148
templates/EmailHeader.php
Normal file
148
templates/EmailHeader.php
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<?
|
||||||
|
$preheader = $preheader ?? null;
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title></title>
|
||||||
|
<style type="text/css">
|
||||||
|
@font-face{
|
||||||
|
font-family: "League Spartan";
|
||||||
|
src: url("https://standardebooks.org/fonts/league-spartan-bold.woff2") format("woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
border-radius: 1em;
|
||||||
|
font-family: "Georgia", serif;
|
||||||
|
hyphens: auto;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.4;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
color: #222222;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 50em;
|
||||||
|
padding: 2em;
|
||||||
|
background-color: #E9E7E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<? if($preheader){ ?>
|
||||||
|
.preheader{
|
||||||
|
display:none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
font-size: 1px;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1px;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
}
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
|
img.logo{
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2{
|
||||||
|
font-family: "League Spartan", "Helvetica", "Arial", sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1em auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1{
|
||||||
|
border-bottom: 3px double #222;
|
||||||
|
font-size: 2em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2{
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-decoration: underline double;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:link,
|
||||||
|
a:visited{
|
||||||
|
color: #222;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover{
|
||||||
|
color: #4f9d85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
text-align: center;
|
||||||
|
border-top: 3px double #222;
|
||||||
|
padding-top: 1em;
|
||||||
|
margin-top: 2em;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer img{
|
||||||
|
margin-top: 1em;
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:link,
|
||||||
|
a.button:visited{
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button{
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .5);
|
||||||
|
font-style: normal;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #4f9d85;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1em 2em;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: "League Spartan", sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
|
||||||
|
box-shadow: 2px 2px 0 rgba(0, 0, 0, .5), 1px 1px 0px rgba(255,255,255, .5) inset;
|
||||||
|
position: relative;
|
||||||
|
text-transform: lowercase;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: calc(1.4em + 2em + 2px);
|
||||||
|
hyphens: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:hover{
|
||||||
|
background-color: #62bfa3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<? if($preheader){ ?><p class="preheader"><?= Formatter::ToPlainText($preheader) ?><? for($i = 0; $i < 150 - strlen($preheader); $i++){ ?>‌ <? } ?></p><? } ?>
|
9
templates/EmailNewsletterConfirmation.php
Normal file
9
templates/EmailNewsletterConfirmation.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?= Template::EmailHeader() ?>
|
||||||
|
<h1>Confirm your newsletter subscription</h1>
|
||||||
|
<p>Thank you for subscribing to the Standard Ebooks newsletter!</p>
|
||||||
|
<p>Please use the button below to confirm your newsletter subscription. You won’t receive our newsletters until you confirm your subscription.</p>
|
||||||
|
<p class="button-row">
|
||||||
|
<a href="<?= $subscriber->Url ?>/confirm" class="button">Yes, subscribe me to the newsletter</a>
|
||||||
|
</p>
|
||||||
|
<p>If you didn’t subscribe to our newsletter, 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.</p>
|
||||||
|
<?= Template::EmailFooter() ?>
|
9
templates/EmailNewsletterConfirmationText.php
Normal file
9
templates/EmailNewsletterConfirmationText.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Confirm your newsletter subscription
|
||||||
|
|
||||||
|
Thank you for subscribing to the Standard Ebooks newsletter!
|
||||||
|
|
||||||
|
Please use the link below to confirm your newsletter subscription. You won’t receive our newsletters until you confirm your subscription.
|
||||||
|
|
||||||
|
<<?= $subscriber->Url ?>/confirm>
|
||||||
|
|
||||||
|
If you didn’t subscribe to our newsletter, 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.
|
22
templates/Error.php
Normal file
22
templates/Error.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
if(!$exception){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exceptions = [];
|
||||||
|
|
||||||
|
if($exception instanceof Exceptions\ValidationException){
|
||||||
|
$exceptions = $exception->Exceptions;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$exceptions[] = $exception;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<ul class="error">
|
||||||
|
<? foreach($exceptions as $ex){ ?>
|
||||||
|
<li>
|
||||||
|
<p><? $message = $ex->getMessage(); if($message == ''){ $message = 'An error occurred.'; } ?><?= Formatter::ToPlainText($message) ?></p>
|
||||||
|
</li>
|
||||||
|
<? } ?>
|
||||||
|
</ul>
|
|
@ -625,6 +625,19 @@ input[type="checkbox"]:focus{
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.error{
|
||||||
|
border: 2px solid #c33b3b;
|
||||||
|
border-radius: .25rem;
|
||||||
|
background: #e14646;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px 0px rgba(0, 0, 0, .75);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.error li:only-child{
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ebooks nav li.highlighted a:focus,
|
.ebooks nav li.highlighted a:focus,
|
||||||
a.button:focus,
|
a.button:focus,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
|
@ -1471,6 +1484,10 @@ input::placeholder{
|
||||||
color: rgba(0, 0, 0, .75);
|
color: rgba(0, 0, 0, .75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.automation-test{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
label.email,
|
label.email,
|
||||||
label.search{
|
label.search{
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -1498,7 +1515,8 @@ input[type="search"]{
|
||||||
box-shadow: 1px 1px 0 rgba(255, 255, 255, .5) inset;
|
box-shadow: 1px 1px 0 rgba(255, 255, 255, .5) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
label[class]:not(.text) input{
|
label.email input,
|
||||||
|
label.search input{
|
||||||
padding-left: 2.5rem;
|
padding-left: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1987,7 +2005,7 @@ article.ebook section aside.donation p::after{
|
||||||
left: -5000px;
|
left: -5000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"]{
|
form[action="/newsletter/subscribers"]{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
@ -1995,21 +2013,32 @@ form[action*="list-manage.com"]{
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"] label.email{
|
form[action="/newsletter/subscribers"] label.email,
|
||||||
|
form[action="/newsletter/subscribers"] label.captcha{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"] ul{
|
form[action="/newsletter/subscribers"] label.captcha div{
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 1rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/newsletter/subscribers"] label.captcha div input{
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/newsletter/subscribers"] ul{
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"] button{
|
form[action="/newsletter/subscribers"] button{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"] fieldset{
|
form[action="/newsletter/subscribers"] fieldset{
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
|
@ -2761,8 +2790,9 @@ aside button.close:active{
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form[action*="list-manage.com"] label.text,
|
form[action="/newsletter/subscribers"] label.text,
|
||||||
form[action*="list-manage.com"] fieldset{
|
form[action="/newsletter/subscribers"] label.captcha,
|
||||||
|
form[action="/newsletter/subscribers"] fieldset{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,16 @@ try{
|
||||||
|
|
||||||
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0 || !is_dir($wwwFilesystemPath)){
|
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0 || !is_dir($wwwFilesystemPath)){
|
||||||
// Ensure the path exists and that the root is in our www directory
|
// Ensure the path exists and that the root is in our www directory
|
||||||
throw new InvalidAuthorException();
|
throw new Exceptions\InvalidAuthorException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$ebooks = Library::GetEbooksByAuthor($wwwFilesystemPath);
|
$ebooks = Library::GetEbooksByAuthor($wwwFilesystemPath);
|
||||||
|
|
||||||
if(sizeof($ebooks) == 0){
|
if(sizeof($ebooks) == 0){
|
||||||
throw new InvalidAuthorException();
|
throw new Exceptions\InvalidAuthorException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(InvalidAuthorException $ex){
|
catch(Exceptions\InvalidAuthorException $ex){
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
include(WEB_ROOT . '/404.php');
|
include(WEB_ROOT . '/404.php');
|
||||||
exit();
|
exit();
|
||||||
|
|
|
@ -14,7 +14,7 @@ try{
|
||||||
|
|
||||||
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0){
|
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0){
|
||||||
// Ensure the path exists and that the root is in our www directory
|
// Ensure the path exists and that the root is in our www directory
|
||||||
throw new InvalidEbookException();
|
throw new Exceptions\InvalidEbookException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Were we passed the author and a work but not the translator?
|
// Were we passed the author and a work but not the translator?
|
||||||
|
@ -28,7 +28,7 @@ try{
|
||||||
// This iterator will do a deep scan on the directory. When we hit another directory, the filename will be "." and the path will contain the directory path.
|
// This iterator will do a deep scan on the directory. When we hit another directory, the filename will be "." and the path will contain the directory path.
|
||||||
// We want to find where the "src" directory is, and the directory directly below that will be the final web URL we're looking for.
|
// We want to find where the "src" directory is, and the directory directly below that will be the final web URL we're looking for.
|
||||||
if($file->getFilename() == '.' && preg_match('|/src$|ius', $file->getPath())){
|
if($file->getFilename() == '.' && preg_match('|/src$|ius', $file->getPath())){
|
||||||
throw new SeeOtherEbookException(preg_replace(['|' . WEB_ROOT . '|ius', '|/src$|ius'], '', $file->getPath()));
|
throw new Exceptions\SeeOtherEbookException(preg_replace(['|' . WEB_ROOT . '|ius', '|/src$|ius'], '', $file->getPath()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,12 +60,12 @@ try{
|
||||||
$i++;
|
$i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(SeeOtherEbookException $ex){
|
catch(Exceptions\SeeOtherEbookException $ex){
|
||||||
http_response_code(301);
|
http_response_code(301);
|
||||||
header('Location: ' . $ex->Url);
|
header('Location: ' . $ex->Url);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
catch(InvalidEbookException $ex){
|
catch(Exceptions\InvalidEbookException $ex){
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
include(WEB_ROOT . '/404.php');
|
include(WEB_ROOT . '/404.php');
|
||||||
exit();
|
exit();
|
||||||
|
|
|
@ -83,7 +83,7 @@ try{
|
||||||
$pageHeader = 'Free ebooks in the ' . Formatter::ToPlainText($collectionName) . ' ' . $collectionType;
|
$pageHeader = 'Free ebooks in the ' . Formatter::ToPlainText($collectionName) . ' ' . $collectionType;
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
throw new InvalidCollectionException();
|
throw new Exceptions\InvalidCollectionException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
|
@ -127,7 +127,7 @@ try{
|
||||||
|
|
||||||
$queryString = preg_replace('/^&/ius', '', $queryString);
|
$queryString = preg_replace('/^&/ius', '', $queryString);
|
||||||
}
|
}
|
||||||
catch(InvalidCollectionException $ex){
|
catch(Exceptions\InvalidCollectionException $ex){
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
include(WEB_ROOT . '/404.php');
|
include(WEB_ROOT . '/404.php');
|
||||||
exit();
|
exit();
|
||||||
|
|
17
www/images/captcha.php
Normal file
17
www/images/captcha.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
use Gregwar\Captcha\CaptchaBuilder;
|
||||||
|
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
header('Content-type: image/jpeg');
|
||||||
|
|
||||||
|
$builder = new CaptchaBuilder;
|
||||||
|
$builder->build(CAPTCHA_IMAGE_WIDTH, CAPTCHA_IMAGE_HEIGHT);
|
||||||
|
|
||||||
|
$_SESSION['captcha'] = $builder->getPhrase();
|
||||||
|
|
||||||
|
$builder->output();
|
|
@ -1,43 +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 class="has-hero">
|
|
||||||
<hgroup>
|
|
||||||
<h1>Subscribe to the Newsletter</h1>
|
|
||||||
<h2>to receive missives from the vanguard of digital literature</h2>
|
|
||||||
</hgroup>
|
|
||||||
<picture>
|
|
||||||
<source srcset="/images/the-newsletter@2x.avif 2x, /images/the-newsletter.avif 1x" type="image/avif"/>
|
|
||||||
<source srcset="/images/the-newsletter@2x.jpg 2x, /images/the-newsletter.jpg 1x" type="image/jpg"/>
|
|
||||||
<img src="/images/the-newsletter@2x.jpg" alt="An old man in Renaissance-era costume reading a sheet of paper."/>
|
|
||||||
</picture>
|
|
||||||
<p>Subscribe to receive news, updates, and more from Standard Ebooks. Your information will never be shared, and you can unsubscribe at any time.</p>
|
|
||||||
<form action="https://standardebooks.us7.list-manage.com/subscribe/post?u=da307dcb73c74f6a3d597f056&id=f8832654aa" method="post">
|
|
||||||
<!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
|
|
||||||
<div class="anti-spam" aria-hidden="true"><input type="text" name="b_da307dcb73c74f6a3d597f056_f8832654aa" tabindex="-1" value=""/></div>
|
|
||||||
<label class="email">Email
|
|
||||||
<input type="email" name="EMAIL" value="" required="required"/>
|
|
||||||
</label>
|
|
||||||
<label class="text">First name
|
|
||||||
<input type="text" name="FNAME" value=""/>
|
|
||||||
</label>
|
|
||||||
<label class="text">Last name
|
|
||||||
<input type="text" name="LNAME" value=""/>
|
|
||||||
</label>
|
|
||||||
<fieldset>
|
|
||||||
<p>What kind of email would you like to receive?</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<label class="checkbox"><input type="checkbox" value="1" name="group[78748][1]" checked="checked"/>The occasional Standard Ebooks newsletter</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label class="checkbox"><input type="checkbox" value="2" name="group[78748][2]" checked="checked"/>A monthly summary of new ebook releases</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
|
||||||
<button>Subscribe</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
<?= Template::Footer() ?>
|
|
21
www/newsletter/subscribers/confirm.php
Normal file
21
www/newsletter/subscribers/confirm.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?
|
||||||
|
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! You’ll 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() ?>
|
41
www/newsletter/subscribers/delete.php
Normal file
41
www/newsletter/subscribers/delete.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
use function Safe\preg_match;
|
||||||
|
|
||||||
|
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST;
|
||||||
|
|
||||||
|
try{
|
||||||
|
// We may use GET if we're called from an unsubscribe link in an email
|
||||||
|
if(!in_array($_SERVER['REQUEST_METHOD'], ['DELETE', 'GET'])){
|
||||||
|
throw new Exceptions\InvalidRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscriber = NewsletterSubscriber::Get(HttpInput::Str(GET, 'uuid') ?? '');
|
||||||
|
$subscriber->Delete();
|
||||||
|
|
||||||
|
if($requestType == REST){
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidRequestException $ex){
|
||||||
|
http_response_code(405);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidNewsletterSubscriberException $ex){
|
||||||
|
http_response_code(404);
|
||||||
|
if($requestType == WEB){
|
||||||
|
include(WEB_ROOT . '/404.php');
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= Template::Header(['title' => 'You’ve unsubscribed from the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'You’ve unsubscribed from the Standard Ebooks newsletter.']) ?>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<h1>You’ve been unsubscribed</h1>
|
||||||
|
<p>You’ll no longer receive Standard Ebooks email newsletters. Sorry to see you go!</p>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
||||||
|
|
67
www/newsletter/subscribers/new.php
Normal file
67
www/newsletter/subscribers/new.php
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$subscriber = $_SESSION['subscriber'] ?? new NewsletterSubscriber();
|
||||||
|
$exception = $_SESSION['exception'] ?? null;
|
||||||
|
|
||||||
|
if($exception){
|
||||||
|
http_response_code(400);
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= 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 class="has-hero">
|
||||||
|
<hgroup>
|
||||||
|
<h1>Subscribe to the Newsletter</h1>
|
||||||
|
<h2>to receive missives from the vanguard of digital literature</h2>
|
||||||
|
</hgroup>
|
||||||
|
<picture>
|
||||||
|
<source srcset="/images/the-newsletter@2x.avif 2x, /images/the-newsletter.avif 1x" type="image/avif"/>
|
||||||
|
<source srcset="/images/the-newsletter@2x.jpg 2x, /images/the-newsletter.jpg 1x" type="image/jpg"/>
|
||||||
|
<img src="/images/the-newsletter@2x.jpg" alt="An old man in Renaissance-era costume reading a sheet of paper."/>
|
||||||
|
</picture>
|
||||||
|
<p>Subscribe to receive news, updates, and more from Standard Ebooks. Your information will never be shared, and you can unsubscribe at any time.</p>
|
||||||
|
|
||||||
|
<?= Template::Error(['exception' => $exception]) ?>
|
||||||
|
|
||||||
|
<form action="/newsletter/subscribers" 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">Email
|
||||||
|
<input type="email" name="email" value="<?= Formatter::ToPlainText($subscriber->Email) ?>" maxlength="80" required="required" />
|
||||||
|
</label>
|
||||||
|
<label class="text">First name
|
||||||
|
<input type="text" name="firstname" autocomplete="given-name" maxlength="80" value="<?= Formatter::ToPlainText($subscriber->FirstName) ?>" />
|
||||||
|
</label>
|
||||||
|
<label class="text">Last name
|
||||||
|
<input type="text" name="lastname" autocomplete="family-name" maxlength="80" value="<?= Formatter::ToPlainText($subscriber->LastName) ?>" />
|
||||||
|
</label>
|
||||||
|
<label class="captcha">
|
||||||
|
Type the letters in the image
|
||||||
|
<div>
|
||||||
|
<input type="text" name="captcha" required="required" />
|
||||||
|
<img src="/images/captcha" alt="A visual CAPTCHA." height="<?= CAPTCHA_IMAGE_HEIGHT ?>" width="<?= CAPTCHA_IMAGE_WIDTH ?>" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<fieldset>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
<button>Subscribe</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
77
www/newsletter/subscribers/post.php
Normal file
77
www/newsletter/subscribers/post.php
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<?
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
use function Safe\preg_match;
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||||
|
http_response_code(405);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$requestType = preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT']) ? WEB : REST;
|
||||||
|
|
||||||
|
$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');
|
||||||
|
$subscriber->IsSubscribedToSummary = HttpInput::Bool(POST, 'monthlysummary');
|
||||||
|
|
||||||
|
$captcha = $_SESSION['captcha'] ?? null;
|
||||||
|
|
||||||
|
if($captcha === null || mb_strtolower($captcha) !== mb_strtolower(HttpInput::Str(POST, 'captcha', false))){
|
||||||
|
$error = new Exceptions\ValidationException();
|
||||||
|
|
||||||
|
$error->Add(new Exceptions\InvalidCaptchaException());
|
||||||
|
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
12
www/newsletter/subscribers/success.php
Normal file
12
www/newsletter/subscribers/success.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?
|
||||||
|
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 won’t be activated until you click that link—this helps us prevent spam. Thank you!</p>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
|
@ -19,7 +19,7 @@ try{
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Received GitHub webhook.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Received GitHub webhook.');
|
||||||
|
|
||||||
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||||
throw new WebhookException('Expected HTTP POST.');
|
throw new Exceptions\WebhookException('Expected HTTP POST.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$post = file_get_contents('php://input') ?: '';
|
$post = file_get_contents('php://input') ?: '';
|
||||||
|
@ -30,12 +30,12 @@ try{
|
||||||
$hash = $splitHash[1];
|
$hash = $splitHash[1];
|
||||||
|
|
||||||
if(!hash_equals($hash, hash_hmac($hashAlgorithm, $post, preg_replace("/[\r\n]/ius", '', file_get_contents(GITHUB_SECRET_FILE_PATH) ?: '') ?? ''))){
|
if(!hash_equals($hash, hash_hmac($hashAlgorithm, $post, preg_replace("/[\r\n]/ius", '', file_get_contents(GITHUB_SECRET_FILE_PATH) ?: '') ?? ''))){
|
||||||
throw new WebhookException('Invalid GitHub webhook secret.', $post);
|
throw new Exceptions\InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check before we continue.
|
// Sanity check before we continue.
|
||||||
if(!array_key_exists('HTTP_X_GITHUB_EVENT', $_SERVER)){
|
if(!array_key_exists('HTTP_X_GITHUB_EVENT', $_SERVER)){
|
||||||
throw new WebhookException('Couldn\'t understand HTTP request.', $post);
|
throw new Exceptions\WebhookException('Couldn\'t understand HTTP request.', $post);
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = json_decode($post, true);
|
$data = json_decode($post, true);
|
||||||
|
@ -45,20 +45,20 @@ try{
|
||||||
case 'ping':
|
case 'ping':
|
||||||
// Silence on success.
|
// Silence on success.
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: ping.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: ping.');
|
||||||
throw new NoopException();
|
throw new Exceptions\NoopException();
|
||||||
case 'push':
|
case 'push':
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: push.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: push.');
|
||||||
|
|
||||||
// Get the ebook ID. PHP doesn't throw exceptions on invalid array indexes, so check that first.
|
// Get the ebook ID. PHP doesn't throw exceptions on invalid array indexes, so check that first.
|
||||||
if(!array_key_exists('repository', $data) || !array_key_exists('name', $data['repository'])){
|
if(!array_key_exists('repository', $data) || !array_key_exists('name', $data['repository'])){
|
||||||
throw new WebhookException('Couldn\'t understand HTTP POST data.', $post);
|
throw new Exceptions\WebhookException('Couldn\'t understand HTTP POST data.', $post);
|
||||||
}
|
}
|
||||||
|
|
||||||
$repoName = trim($data['repository']['name'], '/');
|
$repoName = trim($data['repository']['name'], '/');
|
||||||
|
|
||||||
if(in_array($repoName, GITHUB_IGNORED_REPOS)){
|
if(in_array($repoName, GITHUB_IGNORED_REPOS)){
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Repo is in ignore list, no action taken.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Repo is in ignore list, no action taken.');
|
||||||
throw new NoopException();
|
throw new Exceptions\NoopException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the filesystem path for the ebook.
|
// Get the filesystem path for the ebook.
|
||||||
|
@ -73,7 +73,7 @@ try{
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!file_exists($dir . '/HEAD')){
|
if(!file_exists($dir . '/HEAD')){
|
||||||
throw new WebhookException('Couldn\'t find repo "' . $repoName . '" in filesystem at "' . $dir . '".', $post);
|
throw new Exceptions\WebhookException('Couldn\'t find repo "' . $repoName . '" in filesystem at "' . $dir . '".', $post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,13 +84,13 @@ try{
|
||||||
|
|
||||||
if($lastCommitSha1 == ''){
|
if($lastCommitSha1 == ''){
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Error getting last local commit. Output: ' . $lastCommitSha1);
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Error getting last local commit. Output: ' . $lastCommitSha1);
|
||||||
throw new WebhookException('Couldn\'t process ebook.', $post);
|
throw new Exceptions\WebhookException('Couldn\'t process ebook.', $post);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
if($data['after'] == $lastCommitSha1){
|
if($data['after'] == $lastCommitSha1){
|
||||||
// This commit is already in our local repo, so silent success
|
// This commit is already in our local repo, so silent success
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Local repo already in sync, no action taken.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Local repo already in sync, no action taken.');
|
||||||
throw new NoopException();
|
throw new Exceptions\NoopException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ try{
|
||||||
exec('sudo --set-home --user se-vcs-bot ' . SITE_ROOT . '/scripts/pull-from-github ' . escapeshellarg($dir) . ' 2>&1', $output, $returnCode);
|
exec('sudo --set-home --user se-vcs-bot ' . SITE_ROOT . '/scripts/pull-from-github ' . escapeshellarg($dir) . ' 2>&1', $output, $returnCode);
|
||||||
if($returnCode != 0){
|
if($returnCode != 0){
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Error pulling from GitHub. Output: ' . implode("\n", $output));
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Error pulling from GitHub. Output: ' . implode("\n", $output));
|
||||||
throw new WebhookException('Couldn\'t process ebook.', $post);
|
throw new Exceptions\WebhookException('Couldn\'t process ebook.', $post);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, '`git pull` from GitHub complete.');
|
Logger::WriteGithubWebhookLogEntry($requestId, '`git pull` from GitHub complete.');
|
||||||
|
@ -120,7 +120,7 @@ try{
|
||||||
exec('sudo --set-home --user se-vcs-bot tsp ' . SITE_ROOT . '/web/scripts/deploy-ebook-to-www' . $lastPushHashFlag . ' ' . escapeshellarg($dir) . ' 2>&1', $output, $returnCode);
|
exec('sudo --set-home --user se-vcs-bot tsp ' . SITE_ROOT . '/web/scripts/deploy-ebook-to-www' . $lastPushHashFlag . ' ' . escapeshellarg($dir) . ' 2>&1', $output, $returnCode);
|
||||||
if($returnCode != 0){
|
if($returnCode != 0){
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Error queueing ebook for deployment to web. Output: ' . implode("\n", $output));
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Error queueing ebook for deployment to web. Output: ' . implode("\n", $output));
|
||||||
throw new WebhookException('Couldn\'t process ebook.', $post);
|
throw new Exceptions\WebhookException('Couldn\'t process ebook.', $post);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Queue for deployment to web complete.');
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Queue for deployment to web complete.');
|
||||||
|
@ -128,13 +128,17 @@ try{
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new WebhookException('Unrecognized GitHub webhook event.', $post);
|
throw new Exceptions\WebhookException('Unrecognized GitHub webhook event.', $post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Success, no content"
|
// "Success, no content"
|
||||||
http_response_code(204);
|
http_response_code(204);
|
||||||
}
|
}
|
||||||
catch(WebhookException $ex){
|
catch(Exceptions\InvalidCredentialsException $ex){
|
||||||
|
// "Forbidden"
|
||||||
|
http_response_code(403);
|
||||||
|
}
|
||||||
|
catch(Exceptions\WebhookException $ex){
|
||||||
// Uh oh, something went wrong!
|
// Uh oh, something went wrong!
|
||||||
// Log detailed error and debugging information locally.
|
// Log detailed error and debugging information locally.
|
||||||
Logger::WriteGithubWebhookLogEntry($requestId, 'Webhook failed! Error: ' . $ex->getMessage());
|
Logger::WriteGithubWebhookLogEntry($requestId, 'Webhook failed! Error: ' . $ex->getMessage());
|
||||||
|
@ -146,7 +150,7 @@ catch(WebhookException $ex){
|
||||||
// "Client error"
|
// "Client error"
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
}
|
}
|
||||||
catch(NoopException $ex){
|
catch(Exceptions\NoopException $ex){
|
||||||
// We arrive here because a special case required us to take no action for the request, but execution also had to be interrupted.
|
// We arrive here because a special case required us to take no action for the request, but execution also had to be interrupted.
|
||||||
// For example, we received a request for a known repo for which we must ignore requests.
|
// For example, we received a request for a known repo for which we must ignore requests.
|
||||||
|
|
||||||
|
|
87
www/webhooks/postmark.php
Normal file
87
www/webhooks/postmark.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
use function Safe\curl_exec;
|
||||||
|
use function Safe\curl_init;
|
||||||
|
use function Safe\curl_setopt;
|
||||||
|
use function Safe\file_get_contents;
|
||||||
|
use function Safe\json_decode;
|
||||||
|
use function Safe\substr;
|
||||||
|
|
||||||
|
// Get a semi-random ID to identify this request within the log.
|
||||||
|
$requestId = substr(sha1(time() . rand()), 0, 8);
|
||||||
|
|
||||||
|
try{
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Received Postmark webhook.');
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD'] != 'POST'){
|
||||||
|
throw new Exceptions\WebhookException('Expected HTTP POST.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = trim(file_get_contents(SITE_ROOT . '/config/secrets/webhooks@postmarkapp.com')) ?: '';
|
||||||
|
|
||||||
|
// Ensure this webhook actually came from Postmark
|
||||||
|
if($apiKey != ($_SERVER['HTTP_X_SE_KEY'] ?? '')){
|
||||||
|
throw new Exceptions\InvalidCredentialsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = json_decode(file_get_contents('php://input') ?: '');
|
||||||
|
|
||||||
|
if(!$post || !property_exists($post, 'RecordType')){
|
||||||
|
throw new Exceptions\WebhookException('Couldn\'t understand HTTP request.', $post);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($post->RecordType == 'SpamComplaint'){
|
||||||
|
// Received when a user marks an email as spam
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Event type: spam complaint.');
|
||||||
|
|
||||||
|
Db::Query('delete from NewsletterSubscribers where Email = ?', [$post->Email]);
|
||||||
|
}
|
||||||
|
elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressSending){
|
||||||
|
// Received when a user clicks Postmark's "Unsubscribe" link in a newsletter email
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Event type: unsubscribe.');
|
||||||
|
|
||||||
|
$email = $post->Recipient;
|
||||||
|
|
||||||
|
// Remove the email from our newsletter list
|
||||||
|
Db::Query('delete from NewsletterSubscribers where Email = ?', [$email]);
|
||||||
|
|
||||||
|
// Remove the suppression from Postmark, since we deleted it from our own list we will never email them again anyway
|
||||||
|
$handle = curl_init();
|
||||||
|
curl_setopt($handle, CURLOPT_URL, 'https://api.postmarkapp.com/message-streams/' . $post->MessageStream . '/suppressions/delete');
|
||||||
|
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($handle, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Accept: application/json', 'X-Postmark-Server-Token: ' . EMAIL_SMTP_USERNAME]);
|
||||||
|
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, "POST");
|
||||||
|
curl_setopt($handle, CURLOPT_POSTFIELDS, '{"Suppressions": [{"EmailAddress": "' . $email . '"}]}');
|
||||||
|
curl_exec($handle);
|
||||||
|
curl_close($handle);
|
||||||
|
}
|
||||||
|
elseif($post->RecordType == 'SubscriptionChange' && $post->SuppressionReason === null){
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Event type: suppression deletion.');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Unrecognized event: ' . $post->RecordType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Event processed.');
|
||||||
|
|
||||||
|
// "Success, no content"
|
||||||
|
http_response_code(204);
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidCredentialsException $ex){
|
||||||
|
// "Forbidden"
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Invalid key: ' . ($_SERVER['HTTP_X_SE_KEY'] ?? ''));
|
||||||
|
http_response_code(403);
|
||||||
|
}
|
||||||
|
catch(Exceptions\WebhookException $ex){
|
||||||
|
// Uh oh, something went wrong!
|
||||||
|
// Log detailed error and debugging information locally.
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Webhook failed! Error: ' . $ex->getMessage());
|
||||||
|
Logger::WritePostmarkWebhookLogEntry($requestId, 'Webhook POST data: ' . $ex->PostData);
|
||||||
|
|
||||||
|
// Print less details to the client.
|
||||||
|
print($ex->getMessage());
|
||||||
|
|
||||||
|
// "Client error"
|
||||||
|
http_response_code(400);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue