385 lines
14 KiB
PHP
385 lines
14 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Newsletter;
|
|
|
|
use Core\Http\Response;
|
|
use Core\Services\Auth;
|
|
use Core\Services\Database;
|
|
use Core\Services\Mailer;
|
|
use Core\Services\Settings;
|
|
use Core\Views\View;
|
|
use PDO;
|
|
use Throwable;
|
|
|
|
class NewsletterController
|
|
{
|
|
private View $view;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->view = new View(__DIR__ . '/views');
|
|
}
|
|
|
|
public function subscribe(): Response
|
|
{
|
|
$email = trim((string)($_POST['email'] ?? ''));
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
if ($email === '') {
|
|
return new Response('Missing email', 400);
|
|
}
|
|
|
|
$db = Database::get();
|
|
if ($db instanceof PDO) {
|
|
$stmt = $db->prepare("
|
|
INSERT INTO ac_newsletter_subscribers (email, name, status, source)
|
|
VALUES (:email, :name, 'subscribed', 'form')
|
|
ON DUPLICATE KEY UPDATE name = VALUES(name), status = 'subscribed', unsubscribed_at = NULL
|
|
");
|
|
$stmt->execute([
|
|
':email' => $email,
|
|
':name' => $name !== '' ? $name : null,
|
|
]);
|
|
}
|
|
|
|
$this->syncMailchimp($email, $name);
|
|
|
|
return new Response('Subscribed', 200);
|
|
}
|
|
|
|
public function unsubscribeForm(): Response
|
|
{
|
|
$email = trim((string)($_GET['email'] ?? ''));
|
|
return new Response($this->view->render('site/unsubscribe.php', [
|
|
'title' => 'Unsubscribe',
|
|
'email' => $email,
|
|
'status' => '',
|
|
]));
|
|
}
|
|
|
|
public function unsubscribe(): Response
|
|
{
|
|
$email = trim((string)($_POST['email'] ?? ''));
|
|
$status = 'Email is required.';
|
|
|
|
$db = Database::get();
|
|
if ($db instanceof PDO && $email !== '') {
|
|
$stmt = $db->prepare("UPDATE ac_newsletter_subscribers SET status = 'unsubscribed', unsubscribed_at = NOW() WHERE email = :email");
|
|
$stmt->execute([':email' => $email]);
|
|
$status = 'You have been unsubscribed.';
|
|
}
|
|
|
|
return new Response($this->view->render('site/unsubscribe.php', [
|
|
'title' => 'Unsubscribe',
|
|
'email' => $email,
|
|
'status' => $status,
|
|
]));
|
|
}
|
|
|
|
public function adminIndex(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
$campaigns = [];
|
|
if ($db instanceof PDO) {
|
|
$stmt = $db->query("SELECT id, title, subject, status, sent_at, scheduled_at FROM ac_newsletter_campaigns ORDER BY created_at DESC");
|
|
$campaigns = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
}
|
|
return new Response($this->view->render('admin/index.php', [
|
|
'title' => 'Newsletter',
|
|
'campaigns' => $campaigns,
|
|
]));
|
|
}
|
|
|
|
public function adminSubscribers(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
$subscribers = [];
|
|
if ($db instanceof PDO) {
|
|
$stmt = $db->query("SELECT id, email, name, status, created_at FROM ac_newsletter_subscribers ORDER BY created_at DESC");
|
|
$subscribers = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
}
|
|
return new Response($this->view->render('admin/subscribers.php', [
|
|
'title' => 'Newsletter',
|
|
'subscribers' => $subscribers,
|
|
]));
|
|
}
|
|
|
|
public function adminEdit(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
$campaign = [
|
|
'id' => 0,
|
|
'title' => '',
|
|
'subject' => '',
|
|
'content_html' => '',
|
|
'status' => 'draft',
|
|
'scheduled_at' => '',
|
|
];
|
|
$db = Database::get();
|
|
if ($id > 0 && $db instanceof PDO) {
|
|
$stmt = $db->prepare("SELECT * FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
|
|
$stmt->execute([':id' => $id]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($row) {
|
|
$campaign = $row;
|
|
}
|
|
}
|
|
return new Response($this->view->render('admin/edit.php', [
|
|
'title' => $id > 0 ? 'Edit Campaign' : 'New Campaign',
|
|
'campaign' => $campaign,
|
|
'error' => '',
|
|
]));
|
|
}
|
|
|
|
public function adminSave(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
if (!$db instanceof PDO) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$title = trim((string)($_POST['title'] ?? ''));
|
|
$subject = trim((string)($_POST['subject'] ?? ''));
|
|
$content = (string)($_POST['content_html'] ?? '');
|
|
$scheduledAt = trim((string)($_POST['scheduled_at'] ?? ''));
|
|
|
|
if ($title === '' || $subject === '') {
|
|
return $this->renderEditError($id, $title, $subject, $content, 'Title and subject are required.');
|
|
}
|
|
|
|
try {
|
|
$status = $scheduledAt !== '' ? 'scheduled' : 'draft';
|
|
if ($id > 0) {
|
|
$stmt = $db->prepare("
|
|
UPDATE ac_newsletter_campaigns
|
|
SET title = :title, subject = :subject, content_html = :content,
|
|
status = :status, scheduled_at = :scheduled_at
|
|
WHERE id = :id
|
|
");
|
|
$stmt->execute([
|
|
':title' => $title,
|
|
':subject' => $subject,
|
|
':content' => $content,
|
|
':status' => $status,
|
|
':scheduled_at' => $scheduledAt !== '' ? $scheduledAt : null,
|
|
':id' => $id,
|
|
]);
|
|
} else {
|
|
$stmt = $db->prepare("
|
|
INSERT INTO ac_newsletter_campaigns (title, subject, content_html, status, scheduled_at)
|
|
VALUES (:title, :subject, :content, :status, :scheduled_at)
|
|
");
|
|
$stmt->execute([
|
|
':title' => $title,
|
|
':subject' => $subject,
|
|
':content' => $content,
|
|
':status' => $status,
|
|
':scheduled_at' => $scheduledAt !== '' ? $scheduledAt : null,
|
|
]);
|
|
}
|
|
} catch (Throwable $e) {
|
|
return $this->renderEditError($id, $title, $subject, $content, 'Unable to save campaign.');
|
|
}
|
|
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
public function adminSend(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
if (!$db instanceof PDO) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$stmt = $db->prepare("SELECT id, subject, content_html FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
|
|
$stmt->execute([':id' => $id]);
|
|
$campaign = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$campaign) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
$subStmt = $db->query("SELECT id, email FROM ac_newsletter_subscribers WHERE status = 'subscribed'");
|
|
$subs = $subStmt ? $subStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
|
|
$settings = [
|
|
'smtp_host' => Settings::get('smtp_host'),
|
|
'smtp_port' => Settings::get('smtp_port'),
|
|
'smtp_user' => Settings::get('smtp_user'),
|
|
'smtp_pass' => Settings::get('smtp_pass'),
|
|
'smtp_encryption' => Settings::get('smtp_encryption'),
|
|
'smtp_from_email' => Settings::get('smtp_from_email'),
|
|
'smtp_from_name' => Settings::get('smtp_from_name'),
|
|
];
|
|
|
|
foreach ($subs as $sub) {
|
|
$result = Mailer::send((string)$sub['email'], (string)$campaign['subject'], (string)$campaign['content_html'], $settings);
|
|
$sendStmt = $db->prepare("
|
|
INSERT INTO ac_newsletter_sends (campaign_id, subscriber_id, status, sent_at, error_text)
|
|
VALUES (:campaign_id, :subscriber_id, :status, NOW(), :error_text)
|
|
");
|
|
$sendStmt->execute([
|
|
':campaign_id' => (int)$campaign['id'],
|
|
':subscriber_id' => (int)$sub['id'],
|
|
':status' => $result['ok'] ? 'sent' : 'failed',
|
|
':error_text' => $result['ok'] ? null : (string)$result['error'],
|
|
]);
|
|
}
|
|
|
|
$db->prepare("UPDATE ac_newsletter_campaigns SET status = 'sent', sent_at = NOW() WHERE id = :id")
|
|
->execute([':id' => $id]);
|
|
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
public function adminTestSend(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
if (!$db instanceof PDO) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$email = trim((string)($_POST['test_email'] ?? ''));
|
|
if ($id <= 0 || $email === '') {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter/campaigns/edit?id=' . $id]);
|
|
}
|
|
$stmt = $db->prepare("SELECT subject, content_html FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
|
|
$stmt->execute([':id' => $id]);
|
|
$campaign = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$campaign) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
$settings = [
|
|
'smtp_host' => Settings::get('smtp_host'),
|
|
'smtp_port' => Settings::get('smtp_port'),
|
|
'smtp_user' => Settings::get('smtp_user'),
|
|
'smtp_pass' => Settings::get('smtp_pass'),
|
|
'smtp_encryption' => Settings::get('smtp_encryption'),
|
|
'smtp_from_email' => Settings::get('smtp_from_email'),
|
|
'smtp_from_name' => Settings::get('smtp_from_name'),
|
|
];
|
|
|
|
Mailer::send($email, (string)$campaign['subject'], (string)$campaign['content_html'], $settings);
|
|
|
|
return new Response('', 302, ['Location' => '/admin/newsletter/campaigns/edit?id=' . $id]);
|
|
}
|
|
|
|
public function adminProcessQueue(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$db = Database::get();
|
|
if (!$db instanceof PDO) {
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
$stmt = $db->prepare("
|
|
SELECT id FROM ac_newsletter_campaigns
|
|
WHERE status = 'scheduled' AND scheduled_at IS NOT NULL AND scheduled_at <= NOW()
|
|
");
|
|
$stmt->execute();
|
|
$campaigns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($campaigns as $campaign) {
|
|
$_POST['id'] = (int)$campaign['id'];
|
|
$this->adminSend();
|
|
}
|
|
|
|
return new Response('', 302, ['Location' => '/admin/newsletter']);
|
|
}
|
|
|
|
public function adminDeleteSubscriber(): Response
|
|
{
|
|
if ($guard = $this->guard(['admin', 'manager'])) {
|
|
return $guard;
|
|
}
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$db = Database::get();
|
|
if ($db instanceof PDO && $id > 0) {
|
|
$db->prepare("DELETE FROM ac_newsletter_subscribers WHERE id = :id")->execute([':id' => $id]);
|
|
}
|
|
return new Response('', 302, ['Location' => '/admin/newsletter/subscribers']);
|
|
}
|
|
|
|
private function renderEditError(int $id, string $title, string $subject, string $content, string $error): Response
|
|
{
|
|
$campaign = [
|
|
'id' => $id,
|
|
'title' => $title,
|
|
'subject' => $subject,
|
|
'content_html' => $content,
|
|
];
|
|
return new Response($this->view->render('admin/edit.php', [
|
|
'title' => $id > 0 ? 'Edit Campaign' : 'New Campaign',
|
|
'campaign' => $campaign,
|
|
'error' => $error,
|
|
]));
|
|
}
|
|
|
|
private function guard(array $roles): ?Response
|
|
{
|
|
if (!Auth::check()) {
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
|
}
|
|
if (!Auth::hasRole($roles)) {
|
|
return new Response('', 302, ['Location' => '/admin']);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function syncMailchimp(string $email, string $name): void
|
|
{
|
|
$apiKey = Settings::get('mailchimp_api_key');
|
|
$listId = Settings::get('mailchimp_list_id');
|
|
if ($apiKey === '' || $listId === '') {
|
|
return;
|
|
}
|
|
$parts = explode('-', $apiKey);
|
|
$dc = $parts[1] ?? '';
|
|
if ($dc === '') {
|
|
return;
|
|
}
|
|
|
|
$subscriberHash = md5(strtolower($email));
|
|
$url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$listId}/members/{$subscriberHash}";
|
|
$payload = json_encode([
|
|
'email_address' => $email,
|
|
'status' => 'subscribed',
|
|
'merge_fields' => [
|
|
'FNAME' => $name,
|
|
],
|
|
]);
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_USERPWD, 'user:' . $apiKey);
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
}
|