✨ double-opt-in (#45)
This commit is contained in:
parent
cd4f2b10b2
commit
10f211c240
|
@ -2,6 +2,7 @@
|
|||
declare(strict_types = 1);
|
||||
namespace Kimendisch\Sbgg_Jetzt;
|
||||
use Flake\Error;
|
||||
use Flake\Id64;
|
||||
|
||||
// DECODE REQUEST //
|
||||
// get json string
|
||||
|
@ -35,17 +36,23 @@
|
|||
die();
|
||||
}
|
||||
|
||||
// language
|
||||
$language = $request["language"] ?? "";
|
||||
if(!in_array($language, ["de", "en"])){
|
||||
// verify key
|
||||
$verify_key = $request["verify_key"] ?? null;
|
||||
if(!Id64::is_valid($verify_key)){
|
||||
http_response_code(400);
|
||||
echo("invalid language");
|
||||
echo("invalid verify key");
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// ADD TO MAILING LIST //
|
||||
Newsletter::subscribe(mail_address: $mail_address, language: $language);
|
||||
// TRY SUBSCRIBING //
|
||||
if(!Newsletter::subscribe(mail_address: $mail_address, verify_key: $verify_key)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace Kimendisch\Sbgg_Jetzt;
|
||||
use Flake\Error;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// language
|
||||
$language = $request["language"] ?? "";
|
||||
if(!in_array($language, ["de", "en"])){
|
||||
http_response_code(400);
|
||||
echo("invalid language");
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// VERIFY //
|
||||
Newsletter::verify(mail_address: $mail_address, language: $language);
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
?>
|
2
meta.php
2
meta.php
|
@ -32,10 +32,12 @@
|
|||
static::$route["sbgg.jetzt"] = [
|
||||
["path" => "", "target" => "page/start"],
|
||||
["path" => ":lang", "target" => "page/start"],
|
||||
["path" => "newsletter/subscribe", "target" => "page/newsletter/subscribe"],
|
||||
["path" => "newsletter/unsubscribe", "target" => "page/newsletter/unsubscribe"],
|
||||
|
||||
["path" => "static/:filename", "target" => "api/static"],
|
||||
|
||||
["path" => "api/newsletter/verify", "target" => "api/newsletter/verify.php"],
|
||||
["path" => "api/newsletter/subscribe", "target" => "api/newsletter/subscribe.php"],
|
||||
["path" => "api/newsletter/unsubscribe", "target" => "api/newsletter/unsubscribe.php"],
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
return [
|
||||
"subject" => [
|
||||
"de" => "SBGG.jetzt: Anmeldung Newsletter",
|
||||
"en" => "SBGG.jetzt: Newsletter Subscription"
|
||||
],
|
||||
|
||||
"main" => [
|
||||
"de" => <<<HTML
|
||||
<h2>Anmeldung Bestätigen</h2>
|
||||
|
||||
<p>Du hast soeben die Anmeldung zum SBGG.jetzt Newsletter beantragt. Aus Datenschutz-Gründen musst Du deine E-Mail Adresse verifizieren.</p>
|
||||
|
||||
<p>Um Dich für den Newsletter anzumelden, klicke bitte auf den <i>Verifizieren</i> Link unten.</p>
|
||||
HTML,
|
||||
"en" => <<<HTML
|
||||
<h2>Confirm Subscription</h2>
|
||||
|
||||
<p>You have just requested to subscribe to the SBGG.jetzt newsletter. For privacy reasons, you have to verify your mail address.</p>
|
||||
|
||||
<p>In order to subscribe to the newsletter, please click the <i>Verify</i> link below.</p>
|
||||
HTML
|
||||
]
|
||||
];
|
||||
?>
|
|
@ -82,7 +82,8 @@
|
|||
{{main}}
|
||||
<hr />
|
||||
|
||||
<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>
|
||||
{{template-if-isset-dataset:unsubscribe_key,unsubscribe_link}}
|
||||
{{template-if-isset-dataset:verify_key,verify_link}}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<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>
|
|
@ -0,0 +1 @@
|
|||
<center><a href="{{const:url_prefix}}/newsletter/subscribe?mail_address={{dataset:mail_address_urlencoded}}&key={{dataset:verify_key}}&lang={{dataset:language}}">{{text_verify}}</a></center>
|
|
@ -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_subscribe_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_subscribe_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_subscribe_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_subscribe_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_subscribe_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_subscribe_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");
|
||||
|
||||
// verify_key
|
||||
let verify_key = url_params.get("key");
|
||||
|
||||
|
||||
// SEND API REQUEST //
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/newsletter/subscribe", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify({
|
||||
mail_address: mail_address,
|
||||
verify_key: verify_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");
|
||||
}
|
||||
}
|
|
@ -190,7 +190,7 @@ async function newsletter_submit(){
|
|||
|
||||
// SEND API REQUEST //
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/newsletter/subscribe", true);
|
||||
xhr.open("POST", "/api/newsletter/verify", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify({
|
||||
mail_address: mail_address,
|
||||
|
|
|
@ -379,16 +379,16 @@
|
|||
"en" => "Note: Depending on your mail provider, the newsletter may end up in your spam folder."
|
||||
],
|
||||
"news_newsletter_feedback_wait" => [
|
||||
"de" => "Wird angemeldet",
|
||||
"en" => "Signing up"
|
||||
"de" => "Verifizierungs-Mail wird gesendet",
|
||||
"en" => "Sending verification mail"
|
||||
],
|
||||
"news_newsletter_feedback_success" => [
|
||||
"de" => "Erfolgreich angemeldet",
|
||||
"en" => "Successfully signed up"
|
||||
"de" => "Verifizierungs-Mail versendet",
|
||||
"en" => "Verification mail sent"
|
||||
],
|
||||
"news_newsletter_feedback_failure" => [
|
||||
"de" => "Fehler bei der Anmeldung",
|
||||
"en" => "Failed to sign up"
|
||||
"de" => "Fehler bei der Versendung der Verifizierungs-Mail",
|
||||
"en" => "Failed to send verification mail"
|
||||
],
|
||||
|
||||
"news_social_media_title" => [
|
||||
|
@ -468,6 +468,36 @@
|
|||
|
||||
|
||||
|
||||
"newsletter_subscribe_page_title" => [
|
||||
"de" => "SBGG.jetzt - Newsletter Abonnieren",
|
||||
"en" => "SBGG.jetzt - Subscribe to Newsletter"
|
||||
],
|
||||
|
||||
"newsletter_subscribe_title" => [
|
||||
"de" => "Newsletter Abonnieren",
|
||||
"en" => "Subscribe to Newsletter"
|
||||
],
|
||||
"newsletter_subscribe_submit" => [
|
||||
"de" => "Abonnieren",
|
||||
"en" => "Subscribe"
|
||||
],
|
||||
"newsletter_subscribe_feedback_wait" => [
|
||||
"de" => "Wird angemeldet",
|
||||
"en" => "Subscribing"
|
||||
],
|
||||
"newsletter_subscribe_feedback_success" => [
|
||||
"de" => "Erfolgreich angemeldet",
|
||||
"en" => "Successfully subscribed"
|
||||
],
|
||||
"newsletter_subscribe_feedback_failure" => [
|
||||
"de" => "Fehler bei der Anmeldung",
|
||||
"en" => "Failed to subscribe"
|
||||
],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"newsletter_unsubscribe_page_title" => [
|
||||
"de" => "SBGG.jetzt - Newsletter Abbestellen",
|
||||
"en" => "SBGG.jetzt - Unsubscribe from Newsletter"
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
"de" => "Abbestellen",
|
||||
"en" => "Unsubscribe"
|
||||
],
|
||||
"text_verify" => [
|
||||
"de" => "Verifizieren",
|
||||
"en" => "Verify"
|
||||
],
|
||||
|
||||
"text_imprint" => [
|
||||
"de" => "Impressum",
|
||||
|
@ -52,12 +56,12 @@
|
|||
|
||||
|
||||
/**
|
||||
* Add a new mail address to the mailing list.
|
||||
* Start a newsletter subscription verification process.
|
||||
*
|
||||
* @param string $mail_address Recipient mail address.
|
||||
* @param string $language Desired newsletter language.
|
||||
*/
|
||||
public static function subscribe(string $mail_address, string $language): void {
|
||||
public static function verify(string $mail_address, string $language): void {
|
||||
// OPEN DATABASE //
|
||||
// acquire runlock
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
@ -67,35 +71,205 @@
|
|||
|
||||
|
||||
// MAYBE INITIALIZE DATABASE //
|
||||
if(!is_array($newsletter_db->get("list"))){
|
||||
// initialize as empty array
|
||||
$newsletter_db->set("list", []);
|
||||
self::db_init(db: $newsletter_db);
|
||||
|
||||
|
||||
// VERIFY LOGIC //
|
||||
// cleanup of verify list
|
||||
self::verify_list_cleanup(db: $newsletter_db);
|
||||
|
||||
// decide whether verification should be attempted
|
||||
$do_verify = self::verify_decide(db: $newsletter_db, mail_address: $mail_address, language: $language);
|
||||
|
||||
// read current verify key
|
||||
if($do_verify) $verify_key = $newsletter_db->get(["verify_list", sha1($mail_address), "verify_key"]);
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
if($do_verify){
|
||||
// send verify mail
|
||||
$content = self::content_file_read("0000-00-00-verify");
|
||||
self::send(mail_address: $mail_address, content: $content, dataset: [
|
||||
"language" => $language,
|
||||
"verify_key" => $verify_key
|
||||
]);
|
||||
|
||||
} else {
|
||||
// fake delay
|
||||
usleep(rand(200, 2000) * 1000);
|
||||
}
|
||||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Remove stale entries in verify list.
|
||||
*
|
||||
* @param Dat $db Newsletter database object.
|
||||
*/
|
||||
private static function verify_list_cleanup(Dat $db): void {
|
||||
// GET FULL DATABASE //
|
||||
$db_verify_list = $db->get("verify_list");
|
||||
$time = time();
|
||||
|
||||
|
||||
// REMOVE STALE ATTEMPT TIMESTAMPS //
|
||||
foreach($db_verify_list as $one_key => $one_dataset){
|
||||
$db_verify_list[$one_key]["attempt_list"] = array_values(array_filter($one_dataset["attempt_list"], function($one_attempt) use ($time): bool {
|
||||
// only keep if younger than 24h
|
||||
return ($one_attempt + (24 * 3600) > $time);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// UPDATE DATABASE //
|
||||
$entry_key = ["list", $mail_address];
|
||||
// REMOVE STALE DATASETS //
|
||||
$db_verify_list = array_filter($db_verify_list, function($one_dataset): bool {
|
||||
// check whether there are recent attempts
|
||||
if(sizeof($one_dataset["attempt_list"]) > 0) return true;
|
||||
|
||||
// no reason for this dataset to stay
|
||||
return false;
|
||||
});
|
||||
|
||||
// generate unsubscribe key
|
||||
$unsubscribe_key = Id64::new(length: 16);
|
||||
|
||||
// only allow new entries
|
||||
$is_new = ($newsletter_db->get($entry_key) === null);
|
||||
// SET NEW VALUES //
|
||||
$db->set("verify_list", $db_verify_list);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Manage verify list and decide whether a verify attempt should be made.
|
||||
*
|
||||
* @param Dat $db Newsletter database object.
|
||||
* @param string $mail_address Recipient mail address.
|
||||
* @param string $language Desired newsletter language.
|
||||
*
|
||||
* @return bool Whether verify attempt should be made.
|
||||
*/
|
||||
private static function verify_decide(Dat $db, string $mail_address, string $language): bool {
|
||||
// CHECK MEMBER STATUS //
|
||||
if($db->get(["list", $mail_address]) !== null) return false;
|
||||
|
||||
// maybe add to database
|
||||
if($is_new){
|
||||
$newsletter_db->set($entry_key, [
|
||||
"unsubscribe_key" => $unsubscribe_key,
|
||||
"language" => $language
|
||||
|
||||
// MAYBE CREATE DATASET //
|
||||
$verify_list_entry_key = ["verify_list", sha1($mail_address)];
|
||||
if($db->get($verify_list_entry_key) === null){
|
||||
$db->set($verify_list_entry_key, [
|
||||
"attempt_list" => [],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// CHECK COOLDOWN //
|
||||
// get previous attempts
|
||||
$attempt_list = $db->get($verify_list_entry_key)["attempt_list"];
|
||||
$attempt_count = sizeof($attempt_list);
|
||||
|
||||
// get last attempt timestamp
|
||||
$last_attempt_timestamp = end($attempt_list);
|
||||
|
||||
// get cooldown duration from attempt count
|
||||
$cooldown_duration = ([
|
||||
0 => 0,
|
||||
1 => (5 * 60),
|
||||
2 => (30 * 60),
|
||||
3 => (2 * 3600),
|
||||
4 => (6 * 3600),
|
||||
])[$attempt_count] ?? (12 * 3600);
|
||||
|
||||
// check whether still within cooldown
|
||||
$time = time();
|
||||
if($last_attempt_timestamp + $cooldown_duration > $time) return false;
|
||||
|
||||
|
||||
// UPDATE DATASET //
|
||||
// get old values
|
||||
$dataset = $db->get($verify_list_entry_key);
|
||||
|
||||
// generate new verify key
|
||||
$dataset["verify_key"] = Id64::new(length: 16);
|
||||
|
||||
// add attempt
|
||||
$dataset["attempt_list"][] = $time;
|
||||
|
||||
// update language
|
||||
$dataset["language"] = $language;
|
||||
|
||||
// set new values
|
||||
$db->set($verify_list_entry_key, $dataset);
|
||||
|
||||
|
||||
// SEND VERIFY MAIL //
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check a verify key and add a member to the newsletter.
|
||||
*
|
||||
* @param string $mail_address Member mail address.
|
||||
* @param string $verify_key User-provided verify key.
|
||||
*
|
||||
* @return bool Whether member has successfully been subscribed.
|
||||
*/
|
||||
public static function subscribe(string $mail_address, string $verify_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 //
|
||||
self::db_init(db: $newsletter_db);
|
||||
|
||||
|
||||
// VERIFY LOGIC //
|
||||
// cleanup of verify list
|
||||
self::verify_list_cleanup(db: $newsletter_db);
|
||||
|
||||
// find dataset in verify list
|
||||
$verify_list_dataset = $newsletter_db->get(["verify_list", sha1($mail_address)]);
|
||||
$success = ($verify_list_dataset !== null);
|
||||
|
||||
// maybe check verify key
|
||||
$success = ($success and $verify_list_dataset["verify_key"] === $verify_key);
|
||||
|
||||
|
||||
// MAYBE ADD MEMBER //
|
||||
if($success){
|
||||
$entry_key = ["list", $mail_address];
|
||||
|
||||
// generate unsubscribe key
|
||||
$unsubscribe_key = Id64::new(length: 16);
|
||||
|
||||
// only allow new entries
|
||||
$success = ($newsletter_db->get($entry_key) === null);
|
||||
|
||||
// maybe add to database
|
||||
if($success){
|
||||
$newsletter_db->set($entry_key, [
|
||||
"unsubscribe_key" => $unsubscribe_key,
|
||||
"language" => $verify_list_dataset["language"],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
if($is_new){
|
||||
if($success){
|
||||
// send welcome mail
|
||||
$content = self::content_file_read("0000-00-00-welcome");
|
||||
self::send(mail_address: $mail_address, content: $content);
|
||||
|
@ -107,6 +281,9 @@
|
|||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
|
||||
// return
|
||||
return $success;
|
||||
}
|
||||
|
||||
|
||||
|
@ -129,10 +306,7 @@
|
|||
|
||||
|
||||
// MAYBE INITIALIZE DATABASE //
|
||||
if(!is_array($newsletter_db->get("list"))){
|
||||
// initialize as empty array
|
||||
$newsletter_db->set("list", []);
|
||||
}
|
||||
self::db_init(db: $newsletter_db);
|
||||
|
||||
|
||||
// UPDATE DATABASE //
|
||||
|
@ -166,6 +340,25 @@
|
|||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Initialize the database with empty values.
|
||||
*
|
||||
* @param Dat $db Newsletter database object.
|
||||
*/
|
||||
private static function db_init(Dat $db): void {
|
||||
// verify list
|
||||
if(!is_array($db->get("verify_list"))){
|
||||
$db->set("verify_list", []);
|
||||
}
|
||||
|
||||
// member list
|
||||
if(!is_array($db->get("list"))){
|
||||
$db->set("list", []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Read one content file.
|
||||
*
|
||||
|
@ -234,6 +427,7 @@
|
|||
$callback = function(array $matches) use ($content, $dataset): string {
|
||||
$key = $matches["key"];
|
||||
$value = $matches["value"];
|
||||
$value2 = $matches["value2"] ?? "";
|
||||
|
||||
// content value
|
||||
if($key === ""){
|
||||
|
@ -247,6 +441,13 @@
|
|||
return self::content_render(content: $content, dataset: $dataset, root: $value);
|
||||
}
|
||||
|
||||
// conditional template
|
||||
if($key === "template-if-isset-dataset"){
|
||||
if(!isset($dataset[$value])) return "";
|
||||
if(in_array($value2, self::$pending_render)) Error::error(message: "Recursive template rendering", data: ["pending_render" => self::$pending_render, "template" => $value2]);
|
||||
return self::content_render(content: $content, dataset: $dataset, root: $value2);
|
||||
}
|
||||
|
||||
// dataset value
|
||||
if($key === "dataset"){
|
||||
if(!isset($dataset[$value])) Error::error(message: "Unable to substitute dataset placeholder", data: ["matches" => $matches, "dataset" => $dataset]);
|
||||
|
@ -263,7 +464,7 @@
|
|||
// unknown key
|
||||
Error::error(message: "Unknown placeholder key", data: ["key" => $key, "value" => $value]);
|
||||
};
|
||||
$html = preg_replace_callback("/(?:{{)(?:(?<key>[0-9a-z-_\.]+)\:)?(?<value>[0-9a-z-_\.]+)(?:}})/", $callback, $html);
|
||||
$html = preg_replace_callback("/(?:{{)(?:(?<key>[0-9a-z-_\.]+)\:)?(?<value>[0-9a-z-_\.]+)(?:\,(?<value2>[0-9a-z-_\.]+))?(?:}})/", $callback, $html);
|
||||
|
||||
|
||||
// DONE //
|
||||
|
|
Loading…
Reference in New Issue