Files
alrahma_sunday_school/app/Controllers/View/BroadcastEmailController.php
T
2026-05-16 13:44:12 -04:00

257 lines
8.9 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 dont 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,
]);
}
}