admin area newsletter send all (#45)

This commit is contained in:
DrMaxNix 2024-02-19 22:53:14 +01:00
parent 3d4d3bd373
commit eeada3aa41
7 changed files with 429 additions and 52 deletions

View File

@ -48,5 +48,6 @@
["path" => "admin/newsletter/:content", "target" => "page/admin/newsletter/content.php"],
["path" => "admin/newsletter/api/send-one", "target" => "page/admin/newsletter/api/send_one.php"],
["path" => "admin/newsletter/api/send-all", "target" => "page/admin/newsletter/api/send_all.php"],
];
?>

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types = 1);
namespace Kimendisch\Sbgg_Jetzt;
use Flake\Csrf;
// 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 //
// csrf token
Csrf::check(token: $request["csrf_token"] ?? "");
// content name
$content_name = $request["content_name"] ?? "";
if(!is_string($content_name)){
http_response_code(400);
echo("invalid content name");
die();
}
if(!preg_match("/^\d{4}-\d{2}-\d{2}(-[a-z0-9]+)+$/", $content_name)){
http_response_code(400);
echo("invalid content name");
die();
}
// TRY SENDING //
// make sure session isn't locked
if(extension_loaded("session")) session_write_close();
// send
if(!Newsletter::send_all(content_name: $content_name)){
http_response_code(200);
echo(json_encode([
"success" => false
]));
die();
}
// POSITIVE RESPONSE //
http_response_code(200);
echo(json_encode([
"success" => true
]));
?>

View File

@ -72,6 +72,7 @@
Page::css("./page/start/style.css");
Page::js(__DIR__ . "/iframe_magic.js");
Page::js(__DIR__ . "/send_one.js");
Page::js(__DIR__ . "/send_all.js");
Page::font("ubuntu");
Page::font("tabler");
@ -171,6 +172,49 @@
</div>
</div>
</div>
<div class="box danger">
<span class="title">Send All (<?= Newsletter::member_count() ?>)</span>
<div id="newsletter-send-all-form-container" class="form-container">
<div id="newsletter-send-all-form" class="form">
<div class="key-value-pair">
<div class="key">
<span class="ti ti-shield"></span>
</div>
<div class="value-list">
<span id="newsletter-send-all-form-safe-code" class="value select-none">&nbsp;</span>
<div class="inputwrapper">
<input id="newsletter-send-all-form-safe-code-repeat" class="value" type="text" name="safe_code" placeholder="Safe Code" autocomplete="off" />
</div>
</div>
</div>
<input id="newsletter-send-all-form-content-name" type="hidden" value="<?= $content_name ?>" />
<input id="newsletter-send-all-form-csrf-token" type="hidden" value="<?= Csrf::token() ?>" />
<button id="newsletter-send-all-form-submit" class="button primary">
<span class="text">Send All</span>
<span class="icon ti ti-chevron-right"></span>
</button>
</div>
<div id="newsletter-send-all-form-feedback" class="form-feedback gone">
<div id="newsletter-send-all-form-feedback-wait" class="form-feedback-wait centertext gone">
<span class="icon spinning ti ti-loader-2"></span>
<span class="text">Sending</span>
</div>
<div id="newsletter-send-all-form-feedback-success" class="form-feedback-success centertext gone">
<span class="icon ti ti-check"></span>
<span class="text">Successfully sent</span>
</div>
<div id="newsletter-send-all-form-feedback-failure" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
<span class="text">Sending failed</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,190 @@
"use strict";
let newsletter_send_all_form;
let newsletter_send_all_safe_code;
let newsletter_send_all_input_safe_code_repeat;
let newsletter_send_all_input_content_name;
let newsletter_send_all_input_csrf_token;
let newsletter_send_all_input_submit;
let newsletter_send_all_feedback;
let newsletter_send_all_feedback_wait;
let newsletter_send_all_feedback_success;
let newsletter_send_all_feedback_failure;
let newsletter_send_all_valid_safe_code = false;
window.addEventListener("load", function(){
// STORE ELEMENTS //
newsletter_send_all_form = document.getElementById("newsletter-send-all-form");
newsletter_send_all_safe_code = document.getElementById("newsletter-send-all-form-safe-code");
newsletter_send_all_input_safe_code_repeat = document.getElementById("newsletter-send-all-form-safe-code-repeat");
newsletter_send_all_input_content_name = document.getElementById("newsletter-send-all-form-content-name");
newsletter_send_all_input_csrf_token = document.getElementById("newsletter-send-all-form-csrf-token");
newsletter_send_all_input_submit = document.getElementById("newsletter-send-all-form-submit");
newsletter_send_all_feedback = document.getElementById("newsletter-send-all-form-feedback");
newsletter_send_all_feedback_wait = document.getElementById("newsletter-send-all-form-feedback-wait");
newsletter_send_all_feedback_success = document.getElementById("newsletter-send-all-form-feedback-success");
newsletter_send_all_feedback_failure = document.getElementById("newsletter-send-all-form-feedback-failure");
// INITIALIZE INPUTS //
newsletter_send_all_init_safe_code();
newsletter_send_all_init_safe_code_repeat();
newsletter_send_all_init_submit();
});
/**
* HELPER: Initialize safe code text.
*/
async function newsletter_send_all_init_safe_code(){
// POPULATE WITH RANDOM NUMBER //
newsletter_send_all_safe_code.textContent = Math.random().toString().slice(2, 6);
}
/**
* HELPER: Initialize safe code repeat input.
*/
async function newsletter_send_all_init_safe_code_repeat(){
// REGISTER INPUT HANDLER //
newsletter_send_all_input_safe_code_repeat.addEventListener("input", newsletter_send_all_update_safe_code_repeat);
}
/**
* HELPER: Initialize submit button input.
*/
async function newsletter_send_all_init_submit(){
// REGISTER CLICK HANDLER //
newsletter_send_all_input_submit.addEventListener("click", newsletter_send_all_submit);
// UPDATE STATE //
newsletter_send_all_update_submit();
}
/**
* CALLBACK: Update safe code repeat state.
*/
async function newsletter_send_all_update_safe_code_repeat(){
// VALIDATE INPUT //
// load values
let should = newsletter_send_all_safe_code.textContent;
let is = newsletter_send_all_input_safe_code_repeat.value;
/**/console.log({is: is, should: should});
// compare
newsletter_send_all_valid_safe_code = (is === should);
// UPDATE VALIDITY INDICATOR //
// get element
let validity_indicator = newsletter_send_all_input_safe_code_repeat.parentElement;
// reset state
validity_indicator.classList.remove("valid", "invalid");
// set state
if(newsletter_send_all_valid_safe_code){
validity_indicator.classList.add("valid");
} else {
validity_indicator.classList.add("invalid");
}
// UPDATE SUBMIT BUTTON //
newsletter_send_all_update_submit();
}
/**
* HELPER: Update submit button state.
*/
async function newsletter_send_all_update_submit(){
// DISABLE //
newsletter_send_all_input_submit.classList.add("disabled");
// MAYBE ENABLE //
if(newsletter_send_all_valid_safe_code){
newsletter_send_all_input_submit.classList.remove("disabled");
}
}
/**
* CALLBACK: Maybe submit the form.
*/
async function newsletter_send_all_submit(){
// MAKE SURE ALL INPUTS ARE VALID //
if(!newsletter_send_all_valid_safe_code) return;
// SHOW WAIT FEEDBACK //
newsletter_send_all_feedback.classList.remove("hidden", "gone");
newsletter_send_all_form.classList.add("hidden");
newsletter_send_all_feedback_wait.classList.remove("hidden", "gone");
// COLLECT VALUES //
// content name
let content_name = newsletter_send_all_input_content_name.value;
// csrf token
let csrf_token = newsletter_send_all_input_csrf_token.value;
// SEND API REQUEST //
var xhr = new XMLHttpRequest();
xhr.open("POST", "/admin/newsletter/api/send-all", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({
content_name: content_name,
csrf_token: csrf_token
}));
xhr.onload = function(){
let success = true;
// validate http status code
if(xhr.status !== 200) success = false;
// check response
let response = null;
if(success){
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_send_all_feedback_wait.classList.add("gone");
newsletter_send_all_feedback_success.classList.remove("hidden", "gone");
return;
}
// negative feedback: no member
if(response !== null && (response.reason ?? "") === "no_member"){
newsletter_send_all_feedback_wait.classList.add("gone");
newsletter_send_all_feedback_failure_no_member.classList.remove("hidden", "gone");
return;
}
// negative feedback: default
newsletter_send_all_feedback_wait.classList.add("gone");
newsletter_send_all_feedback_failure.classList.remove("hidden", "gone");
}
}

View File

@ -1,35 +1,35 @@
"use strict";
let newsletter_form;
let newsletter_input_mail_address;
let newsletter_input_content_name;
let newsletter_input_csrf_token;
let newsletter_input_submit;
let newsletter_feedback;
let newsletter_feedback_wait;
let newsletter_feedback_success;
let newsletter_feedback_failure;
let newsletter_feedback_failure_no_member;
let newsletter_send_one_form;
let newsletter_send_one_input_mail_address;
let newsletter_send_one_input_content_name;
let newsletter_send_one_input_csrf_token;
let newsletter_send_one_input_submit;
let newsletter_send_one_feedback;
let newsletter_send_one_feedback_wait;
let newsletter_send_one_feedback_success;
let newsletter_send_one_feedback_failure;
let newsletter_send_one_feedback_failure_no_member;
let newsletter_valid_mail_address = false;
let newsletter_send_one_valid_mail_address = false;
window.addEventListener("load", function(){
// STORE ELEMENTS //
newsletter_form = document.getElementById("newsletter-send-one-form");
newsletter_input_mail_address = document.getElementById("newsletter-send-one-form-mail-address");
newsletter_input_content_name = document.getElementById("newsletter-send-one-form-content-name");
newsletter_input_csrf_token = document.getElementById("newsletter-send-one-form-csrf-token");
newsletter_input_submit = document.getElementById("newsletter-send-one-form-submit");
newsletter_feedback = document.getElementById("newsletter-send-one-form-feedback");
newsletter_feedback_wait = document.getElementById("newsletter-send-one-form-feedback-wait");
newsletter_feedback_success = document.getElementById("newsletter-send-one-form-feedback-success");
newsletter_feedback_failure = document.getElementById("newsletter-send-one-form-feedback-failure");
newsletter_feedback_failure_no_member = document.getElementById("newsletter-send-one-form-feedback-failure-no-member");
newsletter_send_one_form = document.getElementById("newsletter-send-one-form");
newsletter_send_one_input_mail_address = document.getElementById("newsletter-send-one-form-mail-address");
newsletter_send_one_input_content_name = document.getElementById("newsletter-send-one-form-content-name");
newsletter_send_one_input_csrf_token = document.getElementById("newsletter-send-one-form-csrf-token");
newsletter_send_one_input_submit = document.getElementById("newsletter-send-one-form-submit");
newsletter_send_one_feedback = document.getElementById("newsletter-send-one-form-feedback");
newsletter_send_one_feedback_wait = document.getElementById("newsletter-send-one-form-feedback-wait");
newsletter_send_one_feedback_success = document.getElementById("newsletter-send-one-form-feedback-success");
newsletter_send_one_feedback_failure = document.getElementById("newsletter-send-one-form-feedback-failure");
newsletter_send_one_feedback_failure_no_member = document.getElementById("newsletter-send-one-form-feedback-failure-no-member");
// INITIALIZE INPUTS //
newsletter_init_mail_address();
newsletter_init_submit();
newsletter_send_one_init_mail_address();
newsletter_send_one_init_submit();
});
@ -37,9 +37,9 @@ window.addEventListener("load", function(){
/**
* HELPER: Initialize mail address input.
*/
async function newsletter_init_mail_address(){
async function newsletter_send_one_init_mail_address(){
// REGISTER INPUT HANDLER //
newsletter_input_mail_address.addEventListener("input", newsletter_update_mail_address);
newsletter_send_one_input_mail_address.addEventListener("input", newsletter_send_one_update_mail_address);
}
@ -47,13 +47,13 @@ async function newsletter_init_mail_address(){
/**
* HELPER: Initialize submit button input.
*/
async function newsletter_init_submit(){
async function newsletter_send_one_init_submit(){
// REGISTER CLICK HANDLER //
newsletter_input_submit.addEventListener("click", newsletter_submit);
newsletter_send_one_input_submit.addEventListener("click", newsletter_send_one_submit);
// UPDATE STATE //
newsletter_update_submit();
newsletter_send_one_update_submit();
}
@ -61,24 +61,24 @@ async function newsletter_init_submit(){
/**
* CALLBACK: Update mail address state.
*/
async function newsletter_update_mail_address(){
async function newsletter_send_one_update_mail_address(){
// VALIDATE INPUT //
// load value
let value = newsletter_input_mail_address.value;
let value = newsletter_send_one_input_mail_address.value;
// check against regex
newsletter_valid_mail_address = (value.match(/^[a-zA-Z0-9\.\-\_\+]+@([a-z0-9\-]+\.)+[a-z0-9\-]{2,}$/) !== null);
newsletter_send_one_valid_mail_address = (value.match(/^[a-zA-Z0-9\.\-\_\+]+@([a-z0-9\-]+\.)+[a-z0-9\-]{2,}$/) !== null);
// UPDATE VALIDITY INDICATOR //
// get element
let validity_indicator = newsletter_input_mail_address.parentElement;
let validity_indicator = newsletter_send_one_input_mail_address.parentElement;
// reset state
validity_indicator.classList.remove("valid", "invalid");
// set state
if(newsletter_valid_mail_address){
if(newsletter_send_one_valid_mail_address){
validity_indicator.classList.add("valid");
} else {
validity_indicator.classList.add("invalid");
@ -86,7 +86,7 @@ async function newsletter_update_mail_address(){
// UPDATE SUBMIT BUTTON //
newsletter_update_submit();
newsletter_send_one_update_submit();
}
@ -94,14 +94,14 @@ async function newsletter_update_mail_address(){
/**
* HELPER: Update submit button state.
*/
async function newsletter_update_submit(){
async function newsletter_send_one_update_submit(){
// DISABLE //
newsletter_input_submit.classList.add("disabled");
newsletter_send_one_input_submit.classList.add("disabled");
// MAYBE ENABLE //
if(newsletter_valid_mail_address){
newsletter_input_submit.classList.remove("disabled");
if(newsletter_send_one_valid_mail_address){
newsletter_send_one_input_submit.classList.remove("disabled");
}
}
@ -110,26 +110,26 @@ async function newsletter_update_submit(){
/**
* CALLBACK: Maybe submit the form.
*/
async function newsletter_submit(){
async function newsletter_send_one_submit(){
// MAKE SURE ALL INPUTS ARE VALID //
if(!newsletter_valid_mail_address) return;
if(!newsletter_send_one_valid_mail_address) return;
// SHOW WAIT FEEDBACK //
newsletter_feedback.classList.remove("hidden", "gone");
newsletter_form.classList.add("hidden");
newsletter_feedback_wait.classList.remove("hidden", "gone");
newsletter_send_one_feedback.classList.remove("hidden", "gone");
newsletter_send_one_form.classList.add("hidden");
newsletter_send_one_feedback_wait.classList.remove("hidden", "gone");
// COLLECT VALUES //
// mail address
let mail_address = newsletter_input_mail_address.value;
let mail_address = newsletter_send_one_input_mail_address.value;
// content name
let content_name = newsletter_input_content_name.value;
let content_name = newsletter_send_one_input_content_name.value;
// csrf token
let csrf_token = newsletter_input_csrf_token.value;
let csrf_token = newsletter_send_one_input_csrf_token.value;
// SEND API REQUEST //
@ -162,20 +162,20 @@ async function newsletter_submit(){
// positive feedback
if(success){
newsletter_feedback_wait.classList.add("gone");
newsletter_feedback_success.classList.remove("hidden", "gone");
newsletter_send_one_feedback_wait.classList.add("gone");
newsletter_send_one_feedback_success.classList.remove("hidden", "gone");
return;
}
// negative feedback: no member
if(response !== null && (response.reason ?? "") === "no_member"){
newsletter_feedback_wait.classList.add("gone");
newsletter_feedback_failure_no_member.classList.remove("hidden", "gone");
newsletter_send_one_feedback_wait.classList.add("gone");
newsletter_send_one_feedback_failure_no_member.classList.remove("hidden", "gone");
return;
}
// negative feedback: default
newsletter_feedback_wait.classList.add("gone");
newsletter_feedback_failure.classList.remove("hidden", "gone");
newsletter_send_one_feedback_wait.classList.add("gone");
newsletter_send_one_feedback_failure.classList.remove("hidden", "gone");
}
}

View File

@ -133,6 +133,12 @@ span.inline {
display: none !important;
}
.select-none {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
@ -290,6 +296,14 @@ span.inline {
.box.important {
border: 0.5rem solid var(--theme);
}
.box.danger {
border: 0.5rem solid var(--color-red);
}
.box.danger * {
--theme: var(--color-red);
--theme-light: var(--color-red-light);
--theme-dark: var(--color-red-dark);
}
a.box:hover {
background-color: var(--color-gray-dark-dark);
}

View File

@ -369,6 +369,31 @@
/**
* Determine number of members.
*
* @return int Number of members in newsletter list.
*/
public static function member_count(): int {
// LOAD LIST FROM DATABASE //
// open
$old_ignore_user_abort = (bool)ignore_user_abort(true);
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
// read
$member_list = $newsletter_db->get("list") ?? [];
// close
$newsletter_db->close();
ignore_user_abort($old_ignore_user_abort);
// RETURN SIZE //
return sizeof($member_list);
}
/**
* Send content to one newsletter member.
*
@ -396,6 +421,48 @@
/**
* Send content to all newsletter members.
*
* @param string $content_name Name of content to send.
*
* @return bool Whether sending was successful.
*/
public static function send_all(string $content_name): bool {
// LOAD LIST FROM DATABASE //
// open database
$old_ignore_user_abort = (bool)ignore_user_abort(true);
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
// read list
$member_list = $newsletter_db->get("list") ?? [];
// close database
$newsletter_db->close();
// SEND CONTENT //
// read content
$content = self::content_file_read($content_name);
// iterate over all list members
$success = true;
foreach($member_list as $one_member_mail_address => $one_member_dataset){
// send to this member
if(!self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset)) $success = false;
}
// FINALIZE //
// release runlock
ignore_user_abort($old_ignore_user_abort);
// return success status
return $success;
}
/**
* HELPER: Initialize the database with empty values.
*