admin area newsletter send one (#45)

This commit is contained in:
DrMaxNix 2024-02-19 21:25:43 +01:00
parent d5172c58a0
commit 5faa6b83bb
5 changed files with 366 additions and 2 deletions

View File

@ -46,5 +46,7 @@
["path" => "admin/login", "target" => "page/admin/login"],
["path" => "admin/newsletter", "target" => "page/admin/newsletter/overview.php"],
["path" => "admin/newsletter/:content", "target" => "page/admin/newsletter/content.php"],
["path" => "admin/newsletter/api/send-one", "target" => "page/admin/newsletter/api/send_one.php"],
];
?>

View File

@ -0,0 +1,81 @@
<?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"] ?? "");
// 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();
}
// 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();
}
// CHECK WHETHER THIS ADDRESS IS A MEMBER //
if(!Newsletter::is_member(mail_address: $mail_address)){
http_response_code(200);
echo(json_encode([
"success" => false,
"reason" => "no_member"
]));
die();
}
// TRY SENDING //
if(!Newsletter::send_one(mail_address: $mail_address, 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

@ -6,6 +6,7 @@
use Flake\Page;
use Flake\Project;
use Flake\Excuse;
use Flake\Csrf;
// HANDLE AUTHENTICATION //
require("./page/admin/auth_handler.php");
@ -70,6 +71,7 @@
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__ . "/iframe_magic.js");
Page::js(__DIR__ . "/send_one.js");
Page::font("ubuntu");
Page::font("tabler");
@ -117,6 +119,60 @@
</div>
</div>
</div>
<div class="section">
<div class="header">
<span class="icon ti ti-mail-fast"></span>
<span class="text">Delivery</span>
</div>
<div class="content rows">
<div class="box">
<span class="title">Send One</span>
<div id="newsletter-send-one-form-container" class="form-container">
<div id="newsletter-send-one-form" class="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-send-one-form-mail-address" class="value" type="text" placeholder="Member Mail Address" autocomplete="off" />
</div>
</div>
</div>
<input id="newsletter-send-one-form-content-name" type="hidden" value="<?= $content_name ?>" />
<input id="newsletter-send-one-form-csrf-token" type="hidden" value="<?= Csrf::token() ?>" />
<button id="newsletter-send-one-form-submit" class="button primary">
<span class="text">Send One</span>
<span class="icon ti ti-chevron-right"></span>
</button>
</div>
<div id="newsletter-send-one-form-feedback" class="form-feedback gone">
<div id="newsletter-send-one-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-one-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-one-form-feedback-failure" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
<span class="text">Sending failed</span>
</div>
<div id="newsletter-send-one-form-feedback-failure-no-member" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
<span class="text">Not a newsletter member</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,181 @@
"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_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");
// INITIALIZE INPUTS //
newsletter_init_mail_address();
newsletter_init_submit();
});
/**
* HELPER: Initialize mail address input.
*/
async function newsletter_init_mail_address(){
// REGISTER INPUT HANDLER //
newsletter_input_mail_address.addEventListener("input", newsletter_update_mail_address);
}
/**
* HELPER: Initialize submit button input.
*/
async function newsletter_init_submit(){
// REGISTER CLICK HANDLER //
newsletter_input_submit.addEventListener("click", newsletter_submit);
// UPDATE STATE //
newsletter_update_submit();
}
/**
* CALLBACK: Update mail address state.
*/
async function newsletter_update_mail_address(){
// VALIDATE INPUT //
// load value
let value = newsletter_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);
// UPDATE VALIDITY INDICATOR //
// get element
let validity_indicator = newsletter_input_mail_address.parentElement;
// reset state
validity_indicator.classList.remove("valid", "invalid");
// set state
if(newsletter_valid_mail_address){
validity_indicator.classList.add("valid");
} else {
validity_indicator.classList.add("invalid");
}
// UPDATE SUBMIT BUTTON //
newsletter_update_submit();
}
/**
* HELPER: Update submit button state.
*/
async function newsletter_update_submit(){
// DISABLE //
newsletter_input_submit.classList.add("disabled");
// MAYBE ENABLE //
if(newsletter_valid_mail_address){
newsletter_input_submit.classList.remove("disabled");
}
}
/**
* CALLBACK: Maybe submit the form.
*/
async function newsletter_submit(){
// MAKE SURE ALL INPUTS ARE VALID //
if(!newsletter_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");
// COLLECT VALUES //
// mail address
let mail_address = newsletter_input_mail_address.value;
// content name
let content_name = newsletter_input_content_name.value;
// csrf token
let csrf_token = newsletter_input_csrf_token.value;
// SEND API REQUEST //
var xhr = new XMLHttpRequest();
xhr.open("POST", "/admin/newsletter/api/send-one", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({
mail_address: mail_address,
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_feedback_wait.classList.add("gone");
newsletter_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");
return;
}
// negative feedback: default
newsletter_feedback_wait.classList.add("gone");
newsletter_feedback_failure.classList.remove("hidden", "gone");
}
}

View File

@ -274,8 +274,7 @@
if($success){
// send welcome mail
$content = self::content_file_read("0000-00-00-welcome");
self::send(mail_address: $mail_address, content: $content);
self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
} else {
// fake delay
@ -343,6 +342,51 @@
/**
* Check whether a mail address is contained in the newsletter list.
*
* @param string $mail_address Mail address to search for.
*
* @return bool Whether this mail address is a newsletter member.
*/
public static function is_member(string $mail_address): bool {
// TRY LOADING FROM DATABASE //
// open
$old_ignore_user_abort = (bool)ignore_user_abort(true);
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
// read
$dataset = $newsletter_db->get(["list", $mail_address]);
// close
$newsletter_db->close();
ignore_user_abort($old_ignore_user_abort);
// CHECK //
return ($dataset !== null);
}
/**
* Send content to one newsletter member.
*
* @param string $mail_address Member mail address.
* @param string $content_name Name of content to send.
*
* @return bool Whether sending was successful.
*/
public static function send_one(string $mail_address, string $content_name): bool {
// read content
$content = self::content_file_read($content_name);
// send content
return self::send(mail_address: $mail_address, content: $content);
}
/**
* HELPER: Initialize the database with empty values.
*