✨ newsletter unsubscribe page and api (#45)
This commit is contained in:
parent
7c13cc3487
commit
cf063c7e4f
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace Kimendisch\Sbgg_Jetzt;
|
||||
use Flake\Error;
|
||||
use Flake\Id64;
|
||||
|
||||
// DECODE REQUEST //
|
||||
// get json string
|
||||
$json_body = file_get_contents("php://input");
|
||||
if(strlen($json_body) <= 0){
|
||||
http_response_code(400);
|
||||
echo("malformed request body");
|
||||
die();
|
||||
}
|
||||
|
||||
// try decoding json
|
||||
$request = json_decode($json_body, true);
|
||||
if(json_last_error() != JSON_ERROR_NONE){
|
||||
http_response_code(400);
|
||||
echo("malformed request body");
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// VALIDATE VALUES //
|
||||
// mail address
|
||||
$mail_address = $request["mail_address"] ?? "";
|
||||
if(!is_string($mail_address)){
|
||||
http_response_code(400);
|
||||
echo("invalid mail address");
|
||||
die();
|
||||
}
|
||||
if(!preg_match("/^[a-zA-Z0-9\.\-\_\+]+@([a-z0-9\-]+\.)+[a-z0-9\-]{2,}$/", $mail_address)){
|
||||
http_response_code(400);
|
||||
echo("invalid mail address");
|
||||
die();
|
||||
}
|
||||
|
||||
// unsubscribe key
|
||||
$unsubscribe_key = $request["unsubscribe_key"] ?? null;
|
||||
if(!Id64::is_valid($unsubscribe_key)){
|
||||
http_response_code(400);
|
||||
echo("invalid unsubscribe key");
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// REMOVE FROM MAILING LIST //
|
||||
if(!Newsletter::unsubscribe(mail_address: $mail_address, unsubscribe_key: $unsubscribe_key)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
?>
|
11
meta.php
11
meta.php
|
@ -14,6 +14,7 @@
|
|||
static::$ext[] = "project";
|
||||
static::$ext[] = "excuse";
|
||||
static::$ext[] = "error";
|
||||
static::$ext[] = "url";
|
||||
|
||||
|
||||
// ROUTES //
|
||||
|
@ -27,11 +28,13 @@
|
|||
|
||||
// pages
|
||||
static::$route["sbgg.jetzt"] = [
|
||||
["path" => "", "target" => "page/start"],
|
||||
["path" => ":lang", "target" => "page/start"],
|
||||
["path" => "", "target" => "page/start"],
|
||||
["path" => ":lang", "target" => "page/start"],
|
||||
["path" => "newsletter/unsubscribe", "target" => "page/newsletter/unsubscribe"],
|
||||
|
||||
["path" => "api/newsletter/subscribe", "target" => "api/newsletter/subscribe.php"]
|
||||
["path" => "static/:filename", "target" => "api/static"],
|
||||
["path" => "static/:filename", "target" => "api/static"],
|
||||
|
||||
["path" => "api/newsletter/subscribe", "target" => "api/newsletter/subscribe.php"],
|
||||
["path" => "api/newsletter/unsubscribe", "target" => "api/newsletter/unsubscribe.php"],
|
||||
];
|
||||
?>
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
{{main}}
|
||||
<hr />
|
||||
|
||||
<center><a href="{{const:url_prefix}}/api/newsletter/unsubscribe?mail_address={{dataset:mail_address_urlencoded}}&key={{dataset:unsubscribe_key}}&lang={{dataset:language}}">{{text_unsubscribe}}</a></center>
|
||||
<center><a href="{{const:url_prefix}}/newsletter/unsubscribe?mail_address={{dataset:mail_address_urlencoded}}&key={{dataset:unsubscribe_key}}&lang={{dataset:language}}">{{text_unsubscribe}}</a></center>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace Kimendisch\Sbgg_Jetzt;
|
||||
use Flake\Lang;
|
||||
use Flake\Lang_Dict;
|
||||
use Flake\Page;
|
||||
use Flake\Hidden;
|
||||
use Flake\File;
|
||||
use Flake\Project;
|
||||
use Flake\Url;
|
||||
use Flake\Url_Redirect;
|
||||
use Flake\Request;
|
||||
use Flake\Id64;
|
||||
|
||||
// COLLECT REQUEST DATA //
|
||||
// mail address
|
||||
$mail_address = $_GET["mail_address"] ?? null;
|
||||
if(!is_string($mail_address)){
|
||||
Url_Redirect::location("http" . (Request::has_ssl() ? "s" : "") . "://" . Request::domain_raw_full());
|
||||
}
|
||||
|
||||
|
||||
// LANGUAGE MANAGER //
|
||||
// hack: fake get param from url path
|
||||
$param_lang = $_GET["lang"] ?? "de";
|
||||
$_GET["lang"] = $param_lang;
|
||||
|
||||
// initialize
|
||||
$lang = new Lang(list: ["de", "en"], default: "de");
|
||||
|
||||
// load dict
|
||||
$dict = new Lang_Dict($lang);
|
||||
require("./page/strings.php");
|
||||
|
||||
|
||||
// PAGE INIT //
|
||||
Page::start();
|
||||
|
||||
Page::title($dict->get("newsletter_unsubscribe_page_title"));
|
||||
Page::icon("./asset/logo-256.png");
|
||||
|
||||
Page::lang($lang->get());
|
||||
Page::viewport(scale: 1, zoom: true);
|
||||
|
||||
Page::robots(index: false, follow: false);
|
||||
Page::author("Kim Endisch");
|
||||
|
||||
Page::$head["analytics"] = '<script defer data-domain="sbgg.jetzt" src="https://analytics.tjdev.de/js/script.js"></script>';
|
||||
|
||||
Page::css("./page/start/style.css");
|
||||
Page::js(__DIR__ . "/main.js");
|
||||
|
||||
Page::font("ubuntu");
|
||||
Page::font("tabler");
|
||||
?>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div id="news" class="section">
|
||||
<div class="content rows">
|
||||
<div id="newsletter" class="box">
|
||||
<span class="title"><?= $dict->get("newsletter_unsubscribe_title") ?></span>
|
||||
|
||||
<div id="newsletter-signup-form-container">
|
||||
<div id="newsletter-signup-form">
|
||||
<div class="key-value-pair">
|
||||
<div class="key">
|
||||
<span class="ti ti-at"></span>
|
||||
</div>
|
||||
<div class="value-list">
|
||||
<div class="inputwrapper">
|
||||
<input id="newsletter-signup-form-mail-address" class="value" type="text" value="<?= htmlspecialchars($mail_address) ?>" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="newsletter-signup-form-submit" class="button primary">
|
||||
<span class="text"><?= $dict->get("newsletter_unsubscribe_submit") ?></span>
|
||||
<span class="icon ti ti-chevron-right"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="newsletter-signup-form-feedback" class="gone">
|
||||
<div id="newsletter-signup-form-feedback-wait" class="centertext gone">
|
||||
<span class="icon spinning ti ti-loader-2"></span>
|
||||
<span class="text"><?= $dict->get("newsletter_unsubscribe_feedback_wait") ?></span>
|
||||
</div>
|
||||
<div id="newsletter-signup-form-feedback-success" class="centertext gone">
|
||||
<span class="icon ti ti-check"></span>
|
||||
<span class="text"><?= $dict->get("newsletter_unsubscribe_feedback_success") ?></span>
|
||||
</div>
|
||||
<div id="newsletter-signup-form-feedback-failure" class="centertext gone">
|
||||
<span class="icon ti ti-x"></span>
|
||||
<span class="text"><?= $dict->get("newsletter_unsubscribe_feedback_failure") ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="footer">
|
||||
<div class="brand">
|
||||
<img src="<?= File::file("./asset/logo-256.png") ?>" alt="logo" />
|
||||
<span>SBGG.jetzt</span>
|
||||
<a href="https://git.tjdev.de/kimendisch/sbgg.jetzt" target="_blank"><?= $dict->get("text_sourcecode") ?> <i class="ti ti-external-link"></i></a>
|
||||
<span class="version">v<?= Project::version() ?></span>
|
||||
</div>
|
||||
|
||||
<div class="lang">
|
||||
<span><i class="ti ti-world"></i></span>
|
||||
<a <?= ($lang->get() === "de" ? "class=\"selected\"" : "") ?> href="<?= Url::query_modify(remove: ["lang"], add: ["lang=de"]) ?>">DE</a>
|
||||
<span class="delimiter">|</span>
|
||||
<a <?= ($lang->get() === "en" ? "class=\"selected\"" : "") ?> href="<?= Url::query_modify(remove: ["lang"], add: ["lang=en"]) ?>">EN</a>
|
||||
</div>
|
||||
|
||||
<div class="legal">
|
||||
<span>© 2024 Kim Endisch</span>
|
||||
<span class="delimiter">|</span>
|
||||
<a href="<?= $dict->get("link_imprint") ?>" target="_blank"><?= $dict->get("text_imprint") ?> <i class="ti ti-external-link"></i></a>
|
||||
<span class="delimiter">|</span>
|
||||
<a href="<?= $dict->get("link_privacy_policy") ?>" target="_blank"><?= $dict->get("text_privacy_policy") ?> <i class="ti ti-external-link"></i></a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,93 @@
|
|||
"use strict";
|
||||
|
||||
let newsletter_form;
|
||||
let newsletter_input_submit;
|
||||
let newsletter_feedback;
|
||||
let newsletter_feedback_wait;
|
||||
let newsletter_feedback_success;
|
||||
let newsletter_feedback_failure;
|
||||
|
||||
window.addEventListener("load", function(){
|
||||
// STORE ELEMENTS //
|
||||
newsletter_form = document.getElementById("newsletter-signup-form");
|
||||
newsletter_input_submit = document.getElementById("newsletter-signup-form-submit");
|
||||
newsletter_feedback = document.getElementById("newsletter-signup-form-feedback");
|
||||
newsletter_feedback_wait = document.getElementById("newsletter-signup-form-feedback-wait");
|
||||
newsletter_feedback_success = document.getElementById("newsletter-signup-form-feedback-success");
|
||||
newsletter_feedback_failure = document.getElementById("newsletter-signup-form-feedback-failure");
|
||||
|
||||
|
||||
// INITIALIZE INPUTS //
|
||||
newsletter_init_submit();
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Initialize submit button input.
|
||||
*/
|
||||
async function newsletter_init_submit(){
|
||||
// REGISTER CLICK HANDLER //
|
||||
newsletter_input_submit.addEventListener("click", newsletter_submit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* CALLBACK: Maybe submit the form.
|
||||
*/
|
||||
async function newsletter_submit(){
|
||||
// SHOW WAIT FEEDBACK //
|
||||
newsletter_feedback.classList.remove("hidden", "gone");
|
||||
newsletter_form.classList.add("hidden");
|
||||
newsletter_feedback_wait.classList.remove("hidden", "gone");
|
||||
|
||||
|
||||
// COLLECT VALUES //
|
||||
// mail address
|
||||
const url_params = new URLSearchParams(window.location.search);
|
||||
let mail_address = url_params.get("mail_address");
|
||||
|
||||
// unsubscribe_key
|
||||
let unsubscribe_key = url_params.get("key");
|
||||
|
||||
|
||||
// SEND API REQUEST //
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/newsletter/unsubscribe", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify({
|
||||
mail_address: mail_address,
|
||||
unsubscribe_key: unsubscribe_key
|
||||
}));
|
||||
|
||||
xhr.onload = function(){
|
||||
let success = true;
|
||||
|
||||
// validate http status code
|
||||
if(xhr.status !== 200) success = false;
|
||||
|
||||
// check response
|
||||
if(success){
|
||||
let response = null;
|
||||
try {
|
||||
response = JSON.parse(xhr.response);
|
||||
} catch(e){}
|
||||
|
||||
if(typeof response !== "object") success = false;
|
||||
if(success && response === null) success = false;
|
||||
if(success && response.success !== true) success = false;
|
||||
}
|
||||
|
||||
// positive feedback
|
||||
if(success){
|
||||
newsletter_feedback_wait.classList.add("gone");
|
||||
newsletter_feedback_success.classList.remove("hidden", "gone");
|
||||
return;
|
||||
}
|
||||
|
||||
// negative feedback
|
||||
newsletter_feedback_wait.classList.add("gone");
|
||||
newsletter_feedback_failure.classList.remove("hidden", "gone");
|
||||
}
|
||||
}
|
|
@ -501,6 +501,10 @@ span.inline {
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
#newsletter-signup-form-mail-address {
|
||||
min-width: 22rem;
|
||||
}
|
||||
|
||||
#newsletter-signup-form-feedback {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
@ -462,6 +462,36 @@
|
|||
"copylink_hint_text" => [
|
||||
"de" => "Bereichs-Link kopieren",
|
||||
"en" => "Copy section link"
|
||||
]
|
||||
],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"newsletter_unsubscribe_page_title" => [
|
||||
"de" => "SBGG.jetzt - Newsletter Abbestellen",
|
||||
"en" => "SBGG.jetzt - Unsubscribe from Newsletter"
|
||||
],
|
||||
|
||||
"newsletter_unsubscribe_title" => [
|
||||
"de" => "Newsletter Abbestellen",
|
||||
"en" => "Unsubscribe from Newsletter"
|
||||
],
|
||||
"newsletter_unsubscribe_submit" => [
|
||||
"de" => "Abbestellen",
|
||||
"en" => "Unsubscribe"
|
||||
],
|
||||
"newsletter_unsubscribe_feedback_wait" => [
|
||||
"de" => "Wird abgemeldet",
|
||||
"en" => "Unsubscribing"
|
||||
],
|
||||
"newsletter_unsubscribe_feedback_success" => [
|
||||
"de" => "Erfolgreich abgemeldet",
|
||||
"en" => "Successfully unsubscribed"
|
||||
],
|
||||
"newsletter_unsubscribe_feedback_failure" => [
|
||||
"de" => "Fehler bei der Abmeldung",
|
||||
"en" => "Failed to unsubscribe"
|
||||
],
|
||||
]);
|
||||
?>
|
||||
|
|
|
@ -111,6 +111,61 @@
|
|||
|
||||
|
||||
|
||||
/**
|
||||
* Remove a mail address from the mailing list.
|
||||
*
|
||||
* @param string $mail_address Member mail address.
|
||||
* @param string $unsubscribe_key Unsubscribe key (used for authorization).
|
||||
*
|
||||
* @return bool Whether a dataset has successfully been removed.
|
||||
*/
|
||||
public static function unsubscribe(string $mail_address, string $unsubscribe_key): bool {
|
||||
// OPEN DATABASE //
|
||||
// acquire runlock
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
||||
// obtain handle
|
||||
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
|
||||
|
||||
|
||||
// MAYBE INITIALIZE DATABASE //
|
||||
if(!is_array($newsletter_db->get("list"))){
|
||||
// initialize as empty array
|
||||
$newsletter_db->set("list", []);
|
||||
}
|
||||
|
||||
|
||||
// UPDATE DATABASE //
|
||||
$entry_key = ["list", $mail_address];
|
||||
|
||||
// check whether mail address is member of newsletter
|
||||
$dataset = $newsletter_db->get($entry_key);
|
||||
$is_member = ($dataset !== null);
|
||||
|
||||
// validate unsubscribe key
|
||||
$unsubscribe_key_valid = (($dataset["unsubscribe_key"] ?? null) === $unsubscribe_key);
|
||||
|
||||
// maybe remove from database
|
||||
$has_removed = false;
|
||||
if($is_member and $unsubscribe_key_valid){
|
||||
$newsletter_db->unset($entry_key);
|
||||
$has_removed = true;
|
||||
}
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
|
||||
// return
|
||||
return $has_removed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Read one content file.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue