244 lines
11 KiB
PHP
244 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace FecaMailshots\Admin;
|
|
|
|
use FecaMailshots\Application\AttachmentService;
|
|
use FecaMailshots\WordPress\WordPressFacade;
|
|
|
|
final class AttachmentsAdminPage
|
|
{
|
|
use AdminRequestHelpers;
|
|
|
|
private const CAPABILITY = 'edit_pages';
|
|
private const RESULT_OPTION_KEY = 'feca_mailshots_attachments_ui_result';
|
|
private const DRAFT_OPTION_KEY = 'feca_mailshots_attachments_ui_draft';
|
|
private const PAGE_SLUG = 'feca-mailshots-attachments';
|
|
private const NONCE_ACTION = 'feca_mailshots_attachments';
|
|
/** @var callable(): AttachmentService */
|
|
private $serviceFactory;
|
|
private WordPressFacade $wp;
|
|
|
|
/** @param callable(): AttachmentService $serviceFactory */
|
|
public function __construct(callable $serviceFactory, WordPressFacade $wp)
|
|
{
|
|
$this->serviceFactory = $serviceFactory;
|
|
$this->wp = $wp;
|
|
}
|
|
|
|
public function register(): void
|
|
{
|
|
$this->wp->addAction('admin_menu', [$this, 'registerMenu']);
|
|
$this->wp->addAction('admin_post_feca_mailshots_attachments_api', [$this, 'handleApi']);
|
|
$this->wp->addAction('admin_post_feca_mailshots_attachments_ui_save', [$this, 'handleUiSave']);
|
|
$this->wp->addAction('admin_post_feca_mailshots_attachments_ui_delete', [$this, 'handleUiDelete']);
|
|
}
|
|
|
|
public function registerMenu(): void
|
|
{
|
|
$this->wp->addSubmenuPage('feca-mailshot', 'Attachments', 'Attachments', self::CAPABILITY, self::PAGE_SLUG, [$this, 'render']);
|
|
}
|
|
|
|
public function render(): void
|
|
{
|
|
if (!$this->wp->currentUserCan(self::CAPABILITY)) {
|
|
echo 'Permission denied';
|
|
return;
|
|
}
|
|
$items = $this->service()->list();
|
|
$draft = $this->consumeOptionArray(self::DRAFT_OPTION_KEY);
|
|
$editId = (int) ($this->wp->requestParam('edit_id', '0') ?? '0');
|
|
if ($editId <= 0 && is_array($draft) && isset($draft['id'])) {
|
|
$editId = (int) $draft['id'];
|
|
}
|
|
$editItem = null;
|
|
foreach ($items as $row) {
|
|
if ((int) ($row['id'] ?? 0) === $editId) {
|
|
$editItem = $row;
|
|
break;
|
|
}
|
|
}
|
|
$result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
|
|
$action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
|
|
$name = (string) ($editItem['name'] ?? '');
|
|
$fileName = (string) ($editItem['file_name'] ?? '');
|
|
$base64 = '';
|
|
if (is_array($draft)) {
|
|
$name = (string) ($draft['name'] ?? $name);
|
|
$fileName = (string) ($draft['file_name'] ?? $fileName);
|
|
$base64 = (string) ($draft['file_bytes_base64'] ?? '');
|
|
}
|
|
|
|
echo '<div class="wrap"><h1>Attachments</h1>';
|
|
if ($result !== null) {
|
|
$ok = !empty($result['ok']);
|
|
$bg = $ok ? '#f1f8e9' : '#ffebee';
|
|
$border = $ok ? '#8bc34a' : '#ef9a9a';
|
|
$title = $ok ? 'Attachment saved.' : 'Attachment action failed.';
|
|
echo '<div style="padding:10px;border:1px solid ' . $border . ';background:' . $bg . ';margin:12px 0;">';
|
|
echo '<strong>' . htmlspecialchars($title) . '</strong>';
|
|
if (!empty($result['errors']) && is_array($result['errors'])) {
|
|
echo '<p style="margin:6px 0 0 0;">' . htmlspecialchars(implode('; ', $result['errors'])) . '</p>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '<p><button type="button" class="button button-primary" id="att-open-new">New Attachment</button></p>';
|
|
echo '<div id="att-editor-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9998;">';
|
|
echo '<div style="max-width:900px;margin:30px auto;background:#fff;padding:12px;max-height:88vh;overflow:auto;">';
|
|
echo '<p style="text-align:right;margin:0;"><button type="button" class="button" id="att-close-editor">Close</button></p>';
|
|
echo '<form method="post" enctype="multipart/form-data" action="' . $action . '" style="padding:12px;border:1px solid #dcdcde;background:#fff;margin-bottom:12px;" id="att-editor-form">';
|
|
echo '<h2 style="margin-top:0;">' . ($editId > 0 ? 'Edit Attachment' : 'New Attachment') . '</h2>';
|
|
echo $this->modalErrorHtml($result);
|
|
echo '<input type="hidden" name="action" value="feca_mailshots_attachments_ui_save">';
|
|
echo $this->hiddenNonceField(self::NONCE_ACTION);
|
|
if ($editId > 0) {
|
|
echo '<input type="hidden" name="id" value="' . $editId . '" id="att-editor-id">';
|
|
} else {
|
|
echo '<input type="hidden" name="id" value="" id="att-editor-id">';
|
|
}
|
|
echo '<table class="form-table" role="presentation">';
|
|
echo '<tr><th scope="row"><label for="att_name">Name</label></th><td><input class="regular-text" type="text" id="att_name" name="name" value="' . htmlspecialchars($name, ENT_QUOTES) . '"></td></tr>';
|
|
echo '<tr><th scope="row"><label for="att_file_name">File Name</label></th><td><input class="regular-text" type="text" id="att_file_name" name="file_name" value="' . htmlspecialchars($fileName, ENT_QUOTES) . '"></td></tr>';
|
|
echo '<tr><th scope="row"><label for="att_file_upload">Upload File</label></th><td><input type="file" id="att_file_upload" name="file_upload"></td></tr>';
|
|
echo '<tr><th scope="row"><label for="att_base64">File Bytes (Base64)</label></th><td><textarea id="att_base64" name="file_bytes_base64" rows="4" class="large-text code">' . htmlspecialchars($base64) . '</textarea><p class="description">Provide base64 directly, or use file upload above.</p></td></tr>';
|
|
echo '</table>';
|
|
echo '<p><button type="submit" class="button button-primary">' . ($editId > 0 ? 'Update Attachment' : 'Create Attachment') . '</button> ';
|
|
if ($editId > 0) {
|
|
echo '<a class="button" href="' . htmlspecialchars($this->wp->adminUrl('admin.php?page=' . self::PAGE_SLUG)) . '">Cancel Edit</a>';
|
|
}
|
|
echo '</p></form>';
|
|
echo '</div></div>';
|
|
|
|
echo '<h2>Existing Attachments</h2>';
|
|
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Name</th><th>File</th><th>MIME</th><th>Bytes</th><th>Actions</th></tr></thead><tbody>';
|
|
foreach ($items as $row) {
|
|
$id = (int) ($row['id'] ?? 0);
|
|
$editUrl = $this->wp->adminUrl('admin.php?page=feca-mailshots-attachments&edit_id=' . $id);
|
|
echo '<tr>';
|
|
echo '<td>' . $id . '</td>';
|
|
echo '<td>' . htmlspecialchars((string) ($row['name'] ?? '')) . '</td>';
|
|
echo '<td>' . htmlspecialchars((string) ($row['file_name'] ?? '')) . '</td>';
|
|
echo '<td>' . htmlspecialchars((string) ($row['mime_type'] ?? '')) . '</td>';
|
|
echo '<td>' . htmlspecialchars((string) ($row['byte_size'] ?? '')) . '</td>';
|
|
echo '<td><a class="button button-small" href="' . htmlspecialchars($editUrl) . '">Edit</a> ';
|
|
echo '<form method="post" action="' . $action . '" style="display:inline;">';
|
|
echo '<input type="hidden" name="action" value="feca_mailshots_attachments_ui_delete">';
|
|
echo $this->hiddenNonceField(self::NONCE_ACTION);
|
|
echo '<input type="hidden" name="id" value="' . $id . '">';
|
|
echo '<button type="submit" class="button button-small" onclick="return confirm(\'Delete this attachment?\');">Delete</button>';
|
|
echo '</form></td>';
|
|
echo '</tr>';
|
|
}
|
|
if ($items === []) {
|
|
echo '<tr><td colspan="6">No attachments found.</td></tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
echo $this->modalEditorScript(
|
|
'att-editor-modal',
|
|
'att-open-new',
|
|
'att-close-editor',
|
|
'att-editor-form',
|
|
'att-editor-id',
|
|
['att_name', 'att_file_name', 'att_base64'],
|
|
$editId > 0,
|
|
is_array($draft),
|
|
$result !== null,
|
|
!empty($result['ok']),
|
|
null,
|
|
null,
|
|
'att_file_upload',
|
|
'att_file_name'
|
|
);
|
|
echo '</div>';
|
|
}
|
|
|
|
public function handleUiSave(): void
|
|
{
|
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
|
return;
|
|
}
|
|
$id = $this->requestOptionalInt('id');
|
|
$uploadedBase64 = $this->uploadedFileToBase64('file_upload');
|
|
$base64 = $uploadedBase64 !== null ? $uploadedBase64 : $this->requestString('file_bytes_base64', '');
|
|
$payload = $this->attachmentPayload($base64);
|
|
$result = $this->service()->save($id, $payload);
|
|
$this->persistResultAndDraft(self::RESULT_OPTION_KEY, self::DRAFT_OPTION_KEY, $result, ['id' => $id] + $payload);
|
|
$editId = $id;
|
|
if (!empty($result['ok']) && isset($result['id'])) {
|
|
$editId = (int) $result['id'];
|
|
}
|
|
$this->redirectToAdminPage(self::PAGE_SLUG, $editId);
|
|
}
|
|
|
|
public function handleUiDelete(): void
|
|
{
|
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
|
return;
|
|
}
|
|
$id = $this->requestInt('id', 0);
|
|
$result = ['ok' => true];
|
|
try {
|
|
if ($id > 0) {
|
|
$this->service()->delete($id);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$result = ['ok' => false, 'errors' => [$e->getMessage()]];
|
|
}
|
|
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
|
$this->redirectToAdminPage(self::PAGE_SLUG);
|
|
}
|
|
|
|
public function handleApi(): void
|
|
{
|
|
if (!$this->enforceCapabilityOrJson(self::CAPABILITY)) {
|
|
return;
|
|
}
|
|
|
|
$op = $this->requestString('op', 'list');
|
|
|
|
try {
|
|
$this->handleBasicCrudApi(
|
|
$op,
|
|
fn(): array => ['ok' => true, 'items' => $this->service()->list()],
|
|
function (): array {
|
|
if (!$this->mutationGuardValid(self::CAPABILITY, self::NONCE_ACTION)) {
|
|
return ['ok' => false, 'error' => 'Mutation guard rejected request.'];
|
|
}
|
|
$id = $this->requestOptionalInt('id');
|
|
$payload = $this->attachmentPayload($this->requestString('file_bytes_base64', ''));
|
|
return $this->service()->save($id, $payload);
|
|
},
|
|
function (): array {
|
|
if (!$this->mutationGuardValid(self::CAPABILITY, self::NONCE_ACTION)) {
|
|
return ['ok' => false, 'error' => 'Mutation guard rejected request.'];
|
|
}
|
|
$id = $this->requestInt('id', 0);
|
|
$this->service()->delete($id);
|
|
return ['ok' => true];
|
|
}
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
private function service(): AttachmentService
|
|
{
|
|
return ($this->serviceFactory)();
|
|
}
|
|
|
|
/** @return array{name:string,file_name:string,mime_type:string,file_bytes_base64:string} */
|
|
private function attachmentPayload(string $base64): array
|
|
{
|
|
return [
|
|
'name' => $this->requestString('name', ''),
|
|
'file_name' => $this->requestString('file_name', ''),
|
|
'mime_type' => $this->requestString('mime_type', ''),
|
|
'file_bytes_base64' => $base64,
|
|
];
|
|
}
|
|
|
|
}
|