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 '

Attachments

'; if ($result !== null) { $ok = !empty($result['ok']); $bg = $ok ? '#f1f8e9' : '#ffebee'; $border = $ok ? '#8bc34a' : '#ef9a9a'; $title = $ok ? 'Attachment saved.' : 'Attachment action failed.'; echo '
'; echo '' . htmlspecialchars($title) . ''; if (!empty($result['errors']) && is_array($result['errors'])) { echo '

' . htmlspecialchars(implode('; ', $result['errors'])) . '

'; } echo '
'; } echo '

'; echo ''; echo '

Existing Attachments

'; echo ''; foreach ($items as $row) { $id = (int) ($row['id'] ?? 0); $editUrl = $this->wp->adminUrl('admin.php?page=feca-mailshots-attachments&edit_id=' . $id); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } if ($items === []) { echo ''; } echo '
IDNameFileMIMEBytesActions
' . $id . '' . htmlspecialchars((string) ($row['name'] ?? '')) . '' . htmlspecialchars((string) ($row['file_name'] ?? '')) . '' . htmlspecialchars((string) ($row['mime_type'] ?? '')) . '' . htmlspecialchars((string) ($row['byte_size'] ?? '')) . 'Edit '; echo '
'; echo ''; echo $this->hiddenNonceField(self::NONCE_ACTION); echo ''; echo ''; echo '
No attachments found.
'; 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 '
'; } 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, ]; } }