Add newsletter management functionality

This commit is contained in:
Alex Cabal 2022-03-20 17:40:19 -05:00
parent 90ee0a93c9
commit b0197d189a
57 changed files with 1017 additions and 143 deletions

View file

@ -625,6 +625,19 @@ input[type="checkbox"]:focus{
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,
a.button:focus,
input[type="email"]:focus,
@ -1471,6 +1484,10 @@ input::placeholder{
color: rgba(0, 0, 0, .75);
}
label.automation-test{
display: none;
}
label.email,
label.search{
display: block;
@ -1498,7 +1515,8 @@ input[type="search"]{
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;
}
@ -1987,7 +2005,7 @@ article.ebook section aside.donation p::after{
left: -5000px;
}
form[action*="list-manage.com"]{
form[action="/newsletter/subscribers"]{
display: grid;
grid-gap: 1rem;
grid-template-columns: 1fr 1fr;
@ -1995,21 +2013,32 @@ form[action*="list-manage.com"]{
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;
}
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;
}
form[action*="list-manage.com"] button{
form[action="/newsletter/subscribers"] button{
grid-column: 2;
justify-self: end;
margin-left: 0;
}
form[action*="list-manage.com"] fieldset{
form[action="/newsletter/subscribers"] fieldset{
margin-top: 1rem;
grid-column: 1 / span 2;
}
@ -2761,8 +2790,9 @@ aside button.close:active{
margin-top: 0;
}
form[action*="list-manage.com"] label.text,
form[action*="list-manage.com"] fieldset{
form[action="/newsletter/subscribers"] label.text,
form[action="/newsletter/subscribers"] label.captcha,
form[action="/newsletter/subscribers"] fieldset{
grid-column: 1 / span 2;
}

View file

@ -7,16 +7,16 @@ try{
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
throw new InvalidAuthorException();
throw new Exceptions\InvalidAuthorException();
}
$ebooks = Library::GetEbooksByAuthor($wwwFilesystemPath);
if(sizeof($ebooks) == 0){
throw new InvalidAuthorException();
throw new Exceptions\InvalidAuthorException();
}
}
catch(InvalidAuthorException $ex){
catch(Exceptions\InvalidAuthorException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();

View file

@ -14,7 +14,7 @@ try{
if($urlPath == '' || mb_stripos($wwwFilesystemPath, EBOOKS_DIST_PATH) !== 0){
// 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?
@ -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.
// 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())){
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++;
}
}
catch(SeeOtherEbookException $ex){
catch(Exceptions\SeeOtherEbookException $ex){
http_response_code(301);
header('Location: ' . $ex->Url);
exit();
}
catch(InvalidEbookException $ex){
catch(Exceptions\InvalidEbookException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();

View file

@ -83,7 +83,7 @@ try{
$pageHeader = 'Free ebooks in the ' . Formatter::ToPlainText($collectionName) . ' ' . $collectionType;
}
else{
throw new InvalidCollectionException();
throw new Exceptions\InvalidCollectionException();
}
}
else{
@ -127,7 +127,7 @@ try{
$queryString = preg_replace('/^&/ius', '', $queryString);
}
catch(InvalidCollectionException $ex){
catch(Exceptions\InvalidCollectionException $ex){
http_response_code(404);
include(WEB_ROOT . '/404.php');
exit();

17
www/images/captcha.php Normal file
View 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();

View file

@ -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&amp;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() ?>

View 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! Youll now receive Standard Ebooks email newsletters.</p>
<p>To unsubscribe, simply follow the link at the bottom of any of our newsletters, or <a href="<?= $subscriber->Url ?>/delete">click here</a>.</p>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -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' => 'Youve unsubscribed from the Standard Ebooks newsletter', 'highlight' => 'newsletter', 'description' => 'Youve unsubscribed from the Standard Ebooks newsletter.']) ?>
<main>
<article>
<h1>Youve been unsubscribed</h1>
<p>Youll no longer receive Standard Ebooks email newsletters. Sorry to see you go!</p>
</article>
</main>
<?= Template::Footer() ?>

View 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() ?>

View 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);
}
}

View 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 wont be activated until you click that link—this helps us prevent spam. Thank you!</p>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -19,7 +19,7 @@ try{
Logger::WriteGithubWebhookLogEntry($requestId, 'Received GitHub webhook.');
if($_SERVER['REQUEST_METHOD'] != 'POST'){
throw new WebhookException('Expected HTTP POST.');
throw new Exceptions\WebhookException('Expected HTTP POST.');
}
$post = file_get_contents('php://input') ?: '';
@ -30,12 +30,12 @@ try{
$hash = $splitHash[1];
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.
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);
@ -45,20 +45,20 @@ try{
case 'ping':
// Silence on success.
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: ping.');
throw new NoopException();
throw new Exceptions\NoopException();
case 'push':
Logger::WriteGithubWebhookLogEntry($requestId, 'Event type: push.');
// 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'])){
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'], '/');
if(in_array($repoName, GITHUB_IGNORED_REPOS)){
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.
@ -73,7 +73,7 @@ try{
}
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 == ''){
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{
if($data['after'] == $lastCommitSha1){
// This commit is already in our local repo, so silent success
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);
if($returnCode != 0){
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{
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);
if($returnCode != 0){
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{
Logger::WriteGithubWebhookLogEntry($requestId, 'Queue for deployment to web complete.');
@ -128,13 +128,17 @@ try{
break;
default:
throw new WebhookException('Unrecognized GitHub webhook event.', $post);
throw new Exceptions\WebhookException('Unrecognized GitHub webhook event.', $post);
}
// "Success, no content"
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!
// Log detailed error and debugging information locally.
Logger::WriteGithubWebhookLogEntry($requestId, 'Webhook failed! Error: ' . $ex->getMessage());
@ -146,7 +150,7 @@ catch(WebhookException $ex){
// "Client error"
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.
// For example, we received a request for a known repo for which we must ignore requests.

87
www/webhooks/postmark.php Normal file
View 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);
}