recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -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 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,
]);
}
}