runServiceFactory = $runServiceFactory; $this->mailshotServiceFactory = $mailshotServiceFactory; $this->wp = $wp; } public function register(): void { $this->wp->addAction('admin_menu', [$this, 'registerMenu']); $this->wp->addAction('admin_post_feca_mailshots_test_api', [$this, 'handleApi']); $this->wp->addAction('admin_post_feca_mailshots_test_render_ui', [$this, 'handleRenderUi']); $this->wp->addAction('admin_post_feca_mailshots_test_send_ui', [$this, 'handleSendUi']); } public function registerMenu(): void { $this->wp->addSubmenuPage('feca-mailshot', 'Mailshot Test', 'Mailshot Test', self::CAPABILITY, 'feca-mailshots-test', [$this, 'render']); } public function render(): void { if (!$this->wp->currentUserCan(self::CAPABILITY)) { echo 'Permission denied'; return; } $mailshots = $this->mailshotService()->list(); $selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0'); if ($selectedMailshotId <= 0 && $mailshots !== []) { $selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0); } $preview = ['ok' => false, 'rows' => [], 'errors' => ['Create a mailshot before loading recipients.']]; if ($selectedMailshotId > 0) { $preview = $this->runService()->previewRecipients($selectedMailshotId, 100); } $rows = is_array($preview['rows'] ?? null) ? $preview['rows'] : []; $selectedRecipientIndex = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0'); if ($selectedRecipientIndex < -1) { $selectedRecipientIndex = 0; } $defaultEmail = $this->runService()->defaultTestEmail()['default_test_email'] ?? ''; $testEmail = (string) ($this->wp->requestParam('test_email', (string) $defaultEmail) ?? $defaultEmail); $result = $this->result(); if ((string) ($this->wp->requestParam('render_test', '0') ?? '0') === '1') { if ($selectedRecipientIndex < 0) { $result = ['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']]; } else { try { $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget()); $result = $this->runService()->renderTest($selectedMailshotId, $selectedRecipientIndex); } catch (\Throwable $e) { $result = ['ok' => false, 'errors' => ['Render test failed: ' . $e->getMessage()]]; } } $result['ui_action'] = 'render'; } $action = htmlspecialchars($this->wp->adminUrl('admin-post.php')); echo '

Mailshot Test

'; echo $this->renderAdminUiStyles(); echo '

Render with a selected recipient context, then optionally send one test email.

'; if ($result !== null) { $ok = !empty($result['ok']); $bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error'; echo '
'; echo '' . ($ok ? 'Test action succeeded.' : 'Test action failed.') . ''; if (!empty($result['errors']) && is_array($result['errors'])) { echo '

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

'; } if (!empty($result['warnings']) && is_array($result['warnings'])) { echo '

Warnings: ' . htmlspecialchars(implode('; ', $result['warnings'])) . '

'; } if (!empty($result['sent_to'])) { echo '

Sent to: ' . htmlspecialchars((string) $result['sent_to']) . '

'; } if (!empty($result['sent_at'])) { echo '

Sent at: ' . htmlspecialchars((string) $result['sent_at']) . '

'; } if (($result['ui_action'] ?? '') === 'render' && !empty($result['ok']) && is_array($result['rendered'] ?? null)) { echo '

'; } echo '
'; } echo '
'; echo '

1. Select Mailshot

'; echo '
'; echo ''; echo '
'; echo '
'; echo ''; echo '
'; echo '
'; echo '
'; echo ''; echo '
'; if (!empty($preview['errors'])) { echo '
' . htmlspecialchars(implode('; ', $preview['errors'])) . '
'; } echo '
'; echo '
'; echo '

2. Render / Send Test

'; echo $this->hiddenNonceField(self::NONCE_ACTION); echo ''; echo '
'; echo '
'; echo ''; echo '
'; echo '
'; echo ''; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; if ($rows !== []) { $sampleColumns = $this->sampleColumns($rows, 4); echo '
'; echo '

Recipient Sample (First 20)

'; echo '
'; echo ''; foreach ($sampleColumns as $col) { echo ''; } echo ''; foreach (array_slice($rows, 0, 20) as $r) { $rowData = is_array($r['row'] ?? null) ? $r['row'] : []; echo ''; foreach ($sampleColumns as $col) { echo ''; } echo ''; } echo '
#KeyEmail' . htmlspecialchars($col) . '
' . (int) ($r['index'] ?? 0) . '' . htmlspecialchars((string) ($r['recipient_key'] ?? '')) . '' . htmlspecialchars((string) ($r['recipient_email'] ?? '')) . '' . htmlspecialchars($this->displayCellValue($rowData[$col] ?? null)) . '
'; echo '
'; echo '
'; } if ($result !== null && ($result['ui_action'] ?? '') === 'render' && !empty($result['ok']) && is_array($result['rendered'] ?? null)) { $rendered = $result['rendered']; $recipient = $this->selectedRecipientSummary($rows, $selectedRecipientIndex); $recipientText = $recipient['email'] ?? ''; if (($recipient['key'] ?? '') !== '' && strcasecmp((string) ($recipient['key'] ?? ''), (string) ($recipient['email'] ?? '')) !== 0) { $recipientText .= ($recipientText !== '' ? ' | ' : '') . (string) $recipient['key']; } if ($recipientText === '') { $recipientText = '(recipient unavailable)'; } $subject = (string) ($rendered['subject'] ?? ''); $messageHtml = (string) ($rendered['message'] ?? ''); $pdfHtml = (string) ($rendered['pdf_attachment'] ?? ''); echo '
'; echo '
'; echo '

Render Preview

'; echo '

Recipient: ' . htmlspecialchars($recipientText) . '

'; echo '

Subject: ' . htmlspecialchars($subject) . '

'; echo '
'; echo '

Message (HTML)

'; echo '

PDF Attachment (HTML)

'; echo '
'; echo '

'; echo '
'; echo ''; } echo '
'; } public function handleApi(): void { if (!$this->enforceCapabilityOrJson(self::CAPABILITY)) { return; } $op = (string) ($this->wp->requestParam('op', '') ?? ''); $service = $this->runService(); try { if ($op === 'default_test_email') { $this->wp->sendJson(['ok' => true] + $service->defaultTestEmail()); return; } if ($op === 'render_test') { if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) { return; } $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget()); $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0'); $idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0'); if ($idx < 0) { $this->wp->sendJson(['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']]); return; } $this->wp->sendJson($service->renderTest($mailshotId, $idx)); return; } if ($op === 'send_test') { if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) { return; } $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget()); $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0'); $idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0'); $to = (string) ($this->wp->requestParam('test_email', '') ?? ''); if ($idx < 0) { $this->wp->sendJson($service->sendTestAll($mailshotId, $to)); return; } $this->wp->sendJson($service->sendTest($mailshotId, $idx, $to)); return; } $this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400); } catch (\Throwable $e) { $this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500); } } public function handleRenderUi(): void { if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) { return; } $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0'); $idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0'); if ($idx < 0) { $this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true); return; } $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget()); $this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true); } public function handleSendUi(): void { if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) { return; } $mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0'); $idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0'); $email = (string) ($this->wp->requestParam('test_email', '') ?? ''); try { $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget()); if ($idx < 0) { $result = $this->runService()->sendTestAll($mailshotId, $email); } else { $result = $this->runService()->sendTest($mailshotId, $idx, $email); } } catch (\Throwable $e) { $result = ['ok' => false, 'errors' => ['Send test failed: ' . $e->getMessage()]]; } unset($result['rendered']); $result['ui_action'] = 'send'; $this->wp->updateOption(self::RESULT_OPTION_KEY, $result); $this->redirect($mailshotId, $idx, $email); } private function redirect(int $mailshotId, int $recipientIndex, string $testEmail, bool $renderTest = false): void { $url = $this->wp->adminUrl('admin.php?page=feca-mailshots-test&mailshot_id=' . $mailshotId . '&recipient_index=' . $recipientIndex . '&test_email=' . rawurlencode($testEmail)); if ($renderTest) { $url .= '&render_test=1'; } if (!headers_sent()) { header('Location: ' . $url, true, 302); exit; } } /** @return array|null */ private function result(): ?array { $raw = $this->wp->getOption(self::RESULT_OPTION_KEY, null); $this->wp->deleteOption(self::RESULT_OPTION_KEY); return is_array($raw) ? $raw : null; } private function maybeRaiseMemoryLimit(string $target): void { if ($target === '' || !function_exists('ini_get') || !function_exists('ini_set')) { return; } $current = (string) ini_get('memory_limit'); $currentBytes = $this->memoryLimitToBytes($current); $targetBytes = $this->memoryLimitToBytes($target); if ($currentBytes < 0 || $targetBytes <= 0 || $currentBytes >= $targetBytes) { return; } @ini_set('memory_limit', $target); } private function resolveDownloadMemoryLimitTarget(): string { $optionValue = $this->wp->getOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, ''); if (is_string($optionValue)) { $value = trim($optionValue); if ($this->memoryLimitToBytes($value) > 0) { return $value; } } $envValue = getenv(self::DOWNLOAD_MEMORY_LIMIT_ENV); if (is_string($envValue)) { $value = trim($envValue); if ($this->memoryLimitToBytes($value) > 0) { return $value; } } return ''; } private function memoryLimitToBytes(string $limit): int { $value = trim($limit); if ($value === '') { return 0; } if ($value === '-1') { return -1; } $unit = strtolower(substr($value, -1)); if (ctype_alpha($unit)) { $number = (float) substr($value, 0, -1); switch ($unit) { case 'g': return (int) ($number * 1024 * 1024 * 1024); case 'm': return (int) ($number * 1024 * 1024); case 'k': return (int) ($number * 1024); default: return (int) $number; } } return (int) $value; } private function runService(): MailshotRunService { return ($this->runServiceFactory)(); } private function mailshotService(): MailshotService { return ($this->mailshotServiceFactory)(); } /** * @param list> $rows * @return list */ private function sampleColumns(array $rows, int $maxColumns): array { if ($rows === [] || $maxColumns <= 0) { return []; } $firstRow = is_array($rows[0]['row'] ?? null) ? $rows[0]['row'] : []; if ($firstRow === []) { return []; } $firstRecipientKey = trim((string) ($rows[0]['recipient_key'] ?? '')); $firstRecipientEmail = trim((string) ($rows[0]['recipient_email'] ?? '')); $selected = []; $seenValues = []; foreach (array_keys($firstRow) as $key) { $name = (string) $key; if ($name === '') { continue; } if ($this->isIdLikeField($name) || $this->isEmailLikeField($name) || $this->isKeyLikeField($name)) { continue; } $sampleValue = trim($this->displayCellValue($firstRow[$name] ?? null)); if ($sampleValue !== '') { $valueKey = strtolower($sampleValue); if ($valueKey === strtolower($firstRecipientKey) || $valueKey === strtolower($firstRecipientEmail)) { continue; } if (isset($seenValues[$valueKey])) { continue; } $seenValues[$valueKey] = true; } $selected[] = $name; if (count($selected) >= $maxColumns) { break; } } return $selected; } private function isIdLikeField(string $field): bool { $name = strtolower($field); return $name === 'id' || str_ends_with($name, '.id') || str_ends_with($name, '_id') || str_contains($name, 'accountid'); } private function isEmailLikeField(string $field): bool { $name = strtolower($field); return $name === 'email' || str_ends_with($name, '.email') || str_contains($name, 'email'); } private function isKeyLikeField(string $field): bool { $name = strtolower($field); return $name === 'key' || str_ends_with($name, '.key') || str_contains($name, 'recipient_key') || str_contains($name, 'contact_key'); } /** @param mixed $value */ private function displayCellValue($value): string { if ($value === null) { return ''; } if (is_scalar($value)) { return trim((string) $value); } return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''; } /** * @param list> $rows * @return array{index:int,key:string,email:string}|array{} */ private function selectedRecipientSummary(array $rows, int $recipientIndex): array { foreach ($rows as $row) { $idx = (int) ($row['index'] ?? -1); if ($idx !== $recipientIndex) { continue; } return [ 'index' => $idx, 'key' => trim((string) ($row['recipient_key'] ?? '')), 'email' => trim((string) ($row['recipient_email'] ?? '')), ]; } return []; } }