recreate project
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user