257 lines
8.9 KiB
PHP
Executable File
257 lines
8.9 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
|
||
use App\Controllers\BaseController;
|
||
use App\Models\UserModel;
|
||
use App\Services\EmailService;
|
||
|
||
class BroadcastEmailController extends BaseController
|
||
{
|
||
protected UserModel $userModel;
|
||
protected EmailService $mailer;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->userModel = new UserModel();
|
||
$this->mailer = new EmailService(); // use your existing mailer unmodified
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
helper(['form']);
|
||
|
||
// Parents via your role-based function
|
||
$parents = $this->userModel->getParents();
|
||
$parents = array_values(array_filter($parents, static fn($p) => !empty($p['email'])));
|
||
|
||
// Sender list from MAIL_SENDERS directly (no change to EmailService)
|
||
$fromOptions = $this->senderOptionsFromEnv();
|
||
|
||
return view('administrator/broadcast_email', [
|
||
'parents' => $parents,
|
||
'fromOptions' => $fromOptions, // [['key'=>'general','label'=>'Al Rahma Office <office@...>'], ...]
|
||
]);
|
||
}
|
||
|
||
public function send()
|
||
{
|
||
helper(['form']);
|
||
|
||
if (strtolower($this->request->getMethod()) !== 'post') {
|
||
return redirect()->to(site_url('admin/broadcast-email'));
|
||
}
|
||
|
||
$isTestOnly = $this->request->getPost('send_test_only') !== null;
|
||
|
||
$mode = (string) $this->request->getPost('mode'); // 'personalized' | 'standard'
|
||
$subject = trim((string) $this->request->getPost('subject'));
|
||
$fromKey = trim((string) $this->request->getPost('from_key') ?: 'general');
|
||
$body = (string) $this->request->getPost('body_html');
|
||
$body = $this->sanitizeEmailHtml($body);
|
||
|
||
|
||
// Layout options
|
||
$wrapLayout = (bool) $this->request->getPost('wrap_layout');
|
||
$preheader = (string) ($this->request->getPost('preheader') ?? '');
|
||
$ctaText = (string) ($this->request->getPost('cta_text') ?? '');
|
||
$ctaUrl = (string) ($this->request->getPost('cta_url') ?? '');
|
||
|
||
$testEmail = trim((string) $this->request->getPost('test_email'));
|
||
|
||
if ($subject === '' || $body === '') {
|
||
return redirect()->back()->withInput()->with('error', 'Subject and Body are required.');
|
||
}
|
||
|
||
$isPersonalized = ($mode === 'personalized');
|
||
|
||
// --- TEST ONLY ---
|
||
if ($isTestOnly) {
|
||
if ($testEmail === '') {
|
||
return redirect()->back()->withInput()->with('error', 'Provide a test email address.');
|
||
}
|
||
|
||
$recipientName = 'Parent';
|
||
$html = $this->composeEmailHtml(
|
||
$wrapLayout,
|
||
$subject,
|
||
$body,
|
||
$recipientName,
|
||
$preheader,
|
||
$ctaText,
|
||
$ctaUrl,
|
||
$isPersonalized
|
||
);
|
||
|
||
$ok = $this->mailer->send($testEmail, '[TEST] ' . $subject, $html, $fromKey);
|
||
return redirect()->back()->with(
|
||
$ok ? 'message' : 'error',
|
||
$ok ? "Test email sent to {$testEmail}." : "Test email failed (mailer->send() returned false). Check logs."
|
||
);
|
||
}
|
||
|
||
// --- BROADCAST ---
|
||
$rawIds = (array) ($this->request->getPost('parent_ids') ?? []);
|
||
$ids = [];
|
||
foreach ($rawIds as $v) {
|
||
if (is_string($v) && strpos($v, ',') !== false) {
|
||
$ids = array_merge($ids, array_map('intval', explode(',', $v)));
|
||
} else {
|
||
$ids[] = (int) $v;
|
||
}
|
||
}
|
||
$ids = array_values(array_unique(array_filter($ids)));
|
||
|
||
if (empty($ids)) {
|
||
return redirect()->back()->withInput()->with('error', 'Please select at least one parent.');
|
||
}
|
||
|
||
$rows = model(\App\Models\UserModel::class)
|
||
->select('users.id, users.email, CONCAT(users.firstname, " ", users.lastname) AS name')
|
||
->whereIn('users.id', $ids)
|
||
->where('users.email IS NOT NULL AND users.email != ""')
|
||
->findAll();
|
||
|
||
if (empty($rows)) {
|
||
return redirect()->back()->withInput()->with('error', 'No valid parent emails found.');
|
||
}
|
||
|
||
$stats = ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'mode' => $mode];
|
||
|
||
foreach ($rows as $r) {
|
||
$stats['attempted']++;
|
||
$recipientName = $r['name'] ?: 'Parent';
|
||
|
||
$html = $this->composeEmailHtml(
|
||
$wrapLayout,
|
||
$subject,
|
||
$body,
|
||
$recipientName,
|
||
$preheader,
|
||
$ctaText,
|
||
$ctaUrl,
|
||
$isPersonalized
|
||
);
|
||
|
||
$ok = $this->mailer->send($r['email'], $subject, $html, $fromKey);
|
||
$ok ? $stats['sent']++ : $stats['failed']++;
|
||
}
|
||
|
||
$msg = "Broadcast finished. Mode: {$stats['mode']}. Sent: {$stats['sent']}/{$stats['attempted']}. Failures: {$stats['failed']}.";
|
||
return redirect()->to(site_url('admin/broadcast-email'))
|
||
->with($stats['failed'] > 0 ? 'error' : 'message', $msg);
|
||
}
|
||
|
||
|
||
/**
|
||
* Build HTML: optionally wrap $body inside your view('layout/email_layout', ...).
|
||
*/
|
||
private function composeEmailHtml(
|
||
bool $wrap,
|
||
string $subject,
|
||
string $body,
|
||
string $recipientName,
|
||
string $preheader = '',
|
||
string $ctaText = '',
|
||
string $ctaUrl = '',
|
||
bool $doPersonalize = true
|
||
): string {
|
||
$content = $doPersonalize ? str_replace('{{name}}', $recipientName, $body) : $body;
|
||
|
||
if (!$wrap) {
|
||
return $content; // raw body
|
||
}
|
||
|
||
// Render a CHILD view that defines the section your layout expects
|
||
return view('emails/broadcast_wrapper', [
|
||
'subject' => $subject,
|
||
'content' => $content,
|
||
// pass more if you later wire them in your layout
|
||
'preheader' => $preheader,
|
||
'cta_text' => $ctaText,
|
||
'cta_url' => $ctaUrl,
|
||
]);
|
||
}
|
||
|
||
|
||
private function sanitizeEmailHtml(string $html): string
|
||
{
|
||
// Remove <script> and <iframe> blocks (cheap and effective for admin inputs)
|
||
$html = preg_replace('#<(script|iframe)[^>]*>.*?</\1>#is', '', $html);
|
||
// Optionally strip on* event handlers (onclick, etc.)
|
||
$html = preg_replace('/\son\w+="[^"]*"/i', '', $html);
|
||
$html = preg_replace("/\son\w+='[^']*'/i", '', $html);
|
||
return $html;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Read MAIL_SENDERS from .env so we don’t need changes in EmailService.
|
||
* Returns [['key' => 'general', 'label' => 'Al Rahma Office <office@...>'], ...]
|
||
*/
|
||
private function senderOptionsFromEnv(): array
|
||
{
|
||
$json = env('MAIL_SENDERS', '{}');
|
||
$arr = json_decode($json, true);
|
||
if (!is_array($arr) || empty($arr)) {
|
||
// Fallback to SMTP_USER as a single "general" option
|
||
$smtpUser = getenv('SMTP_USER') ?: '';
|
||
$name = 'Al Rahma Sunday School';
|
||
return [['key' => 'general', 'label' => $name . ($smtpUser ? " <{$smtpUser}>" : '')]];
|
||
}
|
||
|
||
$out = [];
|
||
foreach ($arr as $key => $info) {
|
||
$nm = $info['name'] ?? 'Sender';
|
||
$em = $info['email'] ?? '';
|
||
$out[] = ['key' => (string)$key, 'label' => trim($nm . ($em ? " <{$em}>" : ''))];
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
public function uploadImage()
|
||
{
|
||
if (strtolower($this->request->getMethod()) !== 'post') {
|
||
return $this->response->setStatusCode(405)->setJSON(['success' => false, 'error' => 'Method not allowed']);
|
||
}
|
||
|
||
$file = $this->request->getFile('image');
|
||
if (!$file || !$file->isValid()) {
|
||
return $this->response->setStatusCode(400)->setJSON(['success' => false, 'error' => 'No image uploaded']);
|
||
}
|
||
|
||
$mime = strtolower((string) $file->getMimeType());
|
||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp'];
|
||
if (!isset($allowed[$mime])) {
|
||
return $this->response->setStatusCode(415)->setJSON(['success' => false, 'error' => 'Unsupported image type']);
|
||
}
|
||
if ($file->getSize() > 5 * 1024 * 1024) {
|
||
return $this->response->setStatusCode(413)->setJSON(['success' => false, 'error' => 'Image too large (max 5MB)']);
|
||
}
|
||
|
||
$targetDir = rtrim(FCPATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'email';
|
||
if (!is_dir($targetDir)) {
|
||
@mkdir($targetDir, 0755, true);
|
||
}
|
||
|
||
$ext = $allowed[$mime];
|
||
$newName = uniqid('em_', true) . '.' . $ext;
|
||
$file->move($targetDir, $newName, true);
|
||
|
||
helper('url');
|
||
$url = base_url('uploads/email/' . $newName);
|
||
|
||
// 👈 send the rotated token back both in JSON and a response header
|
||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
||
return $this->response
|
||
->setHeader('X-CSRF-HASH', (string) $newHash)
|
||
->setJSON([
|
||
'success' => true,
|
||
'url' => $url,
|
||
'csrf_hash' => $newHash,
|
||
]);
|
||
}
|
||
|
||
}
|