feat(verification): improve expired and invalid code handling + unified view styling
- Updated VerificationController::verify() to: - Distinguish between invalid and expired verification codes - Preserve expired codes to allow resend and proper feedback - Log verification attempts with safe type handling - Display a clear message for already-verified submissions - Avoid DB errors from unknown 'type' values (supports contact+newsletter) - Updated verify_success.php and verify_failed.php: - Unified layout using Arsha theme with View::render() wrapping buffered output - `verify_failed.php` now shows a resend form only if applicable - If code is invalid and no email context is known, redirect prompt is shown - Ensured fallback logic for all messaging variables is robust and user-safe
This commit is contained in:
parent
fc4e8ae851
commit
e09f763db3
|
|
@ -1,9 +1,11 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ContactController.php
|
||||
* Version: 2.8
|
||||
* Version: 2.16
|
||||
* Path: /app/Controllers/ContactController.php
|
||||
* Purpose: Handles contact form submission and verification, including expiration enforcement.
|
||||
* Purpose: Handles contact form submission, abuse checks, and verification logic.
|
||||
* Defers contact/sales email sending until verification.
|
||||
* Tracks newsletter opt-in flag for unified post-verification messaging.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
|
|
@ -14,20 +16,41 @@ use WizdomNetworks\WizeWeb\Utilities\Logger;
|
|||
use WizdomNetworks\WizeWeb\Utilities\Validator;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Sanitizer;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\EmailHelper;
|
||||
use WizdomNetworks\WizeWeb\Utilities\SessionHelper;
|
||||
use WizdomNetworks\WizeWeb\Utilities\SubmissionCheck;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Response;
|
||||
use WizdomNetworks\WizeWeb\Services\EmailService;
|
||||
use WizdomNetworks\WizeWeb\Services\VerificationService;
|
||||
use WizdomNetworks\WizeWeb\Models\ContactModel;
|
||||
use WizdomNetworks\WizeWeb\Services\ContactService;
|
||||
use Exception;
|
||||
|
||||
class ContactController
|
||||
{
|
||||
private EmailService $emailService;
|
||||
private VerificationService $verificationService;
|
||||
|
||||
/**
|
||||
* Initializes email and verification service dependencies.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->emailService = new EmailService();
|
||||
$this->verificationService = new VerificationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the landing page containing the contact form.
|
||||
*/
|
||||
public function index(): void
|
||||
{
|
||||
View::render('pages/landing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form submission: validates, logs, checks abuse, stores, and triggers verification.
|
||||
* If user opted in to the newsletter, flags for follow-up after verification.
|
||||
*/
|
||||
public function submit(): void
|
||||
{
|
||||
Logger::info("Executing controller: ContactController::submit");
|
||||
|
|
@ -42,12 +65,14 @@ class ContactController
|
|||
'message' => Sanitizer::sanitizeString($_POST['message'] ?? ''),
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
'pending_newsletter_opt_in' => isset($_POST['subscribe_newsletter']) && $_POST['subscribe_newsletter'] === '1' ? 1 : 0
|
||||
];
|
||||
|
||||
foreach ($formData as $key => $value) {
|
||||
Logger::info("Sanitized input: {$key} = {$value}");
|
||||
}
|
||||
|
||||
// Validate required fields and email format
|
||||
if (
|
||||
empty($formData['first_name']) ||
|
||||
empty($formData['last_name']) ||
|
||||
|
|
@ -58,26 +83,26 @@ class ContactController
|
|||
!Validator::isEmail($formData['email'])
|
||||
) {
|
||||
Logger::info("Validation failed for contact form submission");
|
||||
$_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
|
||||
$_SESSION['contact_error'] = 'Validation error. Please try again.';
|
||||
SessionHelper::writeClose();
|
||||
header("Location: /#contact");
|
||||
exit;
|
||||
$this->respondOrRedirect(false, 'Validation error.');
|
||||
}
|
||||
|
||||
$db = Database::getConnection();
|
||||
|
||||
// Run submission abuse heuristics
|
||||
$evaluation = SubmissionCheck::evaluate($db, $formData['email'], $formData['phone'], $formData['ip_address']);
|
||||
Logger::info("Submission evaluation result: " . json_encode($evaluation));
|
||||
|
||||
if ($evaluation['action'] === 'block') {
|
||||
$_SESSION['contact_error'] = "Submission blocked due to suspicious activity. If this is a mistake, please contact us directly.";
|
||||
Logger::warning("Blocked submission from IP: {$formData['ip_address']}, Reason: {$evaluation['reason']}");
|
||||
EmailHelper::alertAdmins('Blocked Submission Detected', "A submission was blocked for the following reason: {$evaluation['reason']}", $formData);
|
||||
$this->emailService->alertAdmins('Blocked Submission Detected', $evaluation['reason'], $formData);
|
||||
SessionHelper::writeClose();
|
||||
header("Location: /#contact");
|
||||
exit;
|
||||
$this->respondOrRedirect(false, 'Submission blocked.');
|
||||
}
|
||||
|
||||
// Log submission intent
|
||||
$logId = null;
|
||||
try {
|
||||
$logStmt = $db->prepare("INSERT INTO submission_logs (email, phone, ip_address, user_agent, was_saved, reason) VALUES (:email, :phone, :ip, :ua, :saved, :reason)");
|
||||
|
|
@ -94,46 +119,68 @@ class ContactController
|
|||
Logger::error("Failed to insert into submission_logs: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Save form content
|
||||
$contactModel = new ContactModel($db);
|
||||
$saveSuccess = $contactModel->saveContactForm($formData);
|
||||
|
||||
$contactId = $db->lastInsertId();
|
||||
$verificationCode = bin2hex(random_bytes(16));
|
||||
$expiresAt = (new \DateTime('+72 hours'))->format('Y-m-d H:i:s');
|
||||
|
||||
// Assign verification code
|
||||
if ($saveSuccess) {
|
||||
$stmt = $db->prepare("UPDATE contact_messages SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
|
||||
$stmt->execute([$verificationCode, $expiresAt, $contactId]);
|
||||
ContactService::sendVerificationEmail($formData['email'], $verificationCode);
|
||||
$verificationCode = $this->verificationService->generateCode();
|
||||
$expiresAt = $this->verificationService->getExpirationTime();
|
||||
$this->verificationService->assignCodeToRecord('contact_messages', $contactId, $verificationCode, $expiresAt);
|
||||
|
||||
$this->emailService->sendVerificationEmail(
|
||||
$formData['email'],
|
||||
$verificationCode,
|
||||
'verify_contact',
|
||||
['first_name' => $formData['first_name']]
|
||||
);
|
||||
}
|
||||
|
||||
// Update log if save succeeded
|
||||
if ($saveSuccess && $logId) {
|
||||
$update = $db->prepare("UPDATE submission_logs SET was_saved = 1 WHERE id = :id");
|
||||
$update->execute([':id' => $logId]);
|
||||
}
|
||||
|
||||
// Newsletter opt-in logic
|
||||
if (!empty($_POST['subscribe_newsletter'])) {
|
||||
Logger::info("Contact opted into newsletter: {$formData['email']}");
|
||||
NewsletterService::subscribeOrResend($formData['email']);
|
||||
}
|
||||
|
||||
Logger::info("✅ Writing session flag: contact_success = true");
|
||||
Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
|
||||
|
||||
SessionHelper::writeClose();
|
||||
View::render('pages/contact_check_email');
|
||||
return;
|
||||
$this->respondOrRedirect(true, 'Your message was submitted. Please check your email to verify.');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Fatal error in ContactController::submit: " . $e->getMessage());
|
||||
EmailHelper::alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
|
||||
$this->emailService->alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
|
||||
$_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
|
||||
SessionHelper::writeClose();
|
||||
Logger::info("✅ Writing session flag: catch contact_error = " . $_SESSION['contact_error']);
|
||||
Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
|
||||
header("Location: /#contact");
|
||||
exit;
|
||||
$this->respondOrRedirect(false, 'An internal error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to client depending on request type (AJAX vs standard).
|
||||
* @param bool $success Indicates if the operation succeeded
|
||||
* @param string $message Message to return or display
|
||||
*/
|
||||
private function respondOrRedirect(bool $success, string $message): void
|
||||
{
|
||||
$isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
|
||||
Logger::debug('Detected request type: ' . ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? 'none'));
|
||||
Logger::debug('Will respond with: ' . ($isAjax ? 'JSON' : 'HTML fallback'));
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => $success, 'message' => $message]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
View::render('pages/contact_check_email');
|
||||
} else {
|
||||
header("Location: /#contact");
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ResendVerificationController.php
|
||||
* Version: 1.2
|
||||
* Path: /app/Controllers/ResendVerificationController.php
|
||||
* Purpose: Handles logic for resending verification emails for both newsletter and contact types with rate-limiting and expiration.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Controllers;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
use WizdomNetworks\WizeWeb\Utils\Database;
|
||||
use WizdomNetworks\WizeWeb\Utils\Logger;
|
||||
use WizdomNetworks\WizeWeb\Services\NewsletterService;
|
||||
use WizdomNetworks\WizeWeb\Services\ContactService;
|
||||
|
||||
class ResendVerificationController
|
||||
{
|
||||
public function handle(): void
|
||||
{
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$type = trim($_POST['type'] ?? '');
|
||||
|
||||
if (!$email || !$type || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
View::render('pages/verify_failed', ['reason' => 'Invalid email or type.']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
|
||||
// Rate limit: no more than 3 per day
|
||||
$dailyCheck = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 1 DAY");
|
||||
$dailyCheck->execute([$email, $type]);
|
||||
$dailyCount = $dailyCheck->fetchColumn();
|
||||
|
||||
if ($dailyCount >= 3) {
|
||||
View::render('pages/verify_failed', ['reason' => 'You have reached the daily resend limit. Please try again tomorrow.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rate limit: no more than 1 every 5 minutes
|
||||
$recentCheck = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 5 MINUTE");
|
||||
$recentCheck->execute([$email, $type]);
|
||||
$recentCount = $recentCheck->fetchColumn();
|
||||
|
||||
if ($recentCount > 0) {
|
||||
View::render('pages/verify_failed', ['reason' => 'You must wait a few minutes before requesting another verification email.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log attempt
|
||||
$log = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
|
||||
$log->execute([
|
||||
$email,
|
||||
$type,
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
$code = bin2hex(random_bytes(16));
|
||||
$expiry = (new \DateTime())->modify('+72 hours')->format('Y-m-d H:i:s');
|
||||
|
||||
if ($type === 'newsletter') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
View::render('pages/verify_failed', ['reason' => 'Email is already verified or not found.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $db->prepare("UPDATE subscribers SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
|
||||
$update->execute([$code, $expiry, $row['id']]);
|
||||
NewsletterService::sendVerificationEmail($email, $code);
|
||||
}
|
||||
|
||||
if ($type === 'contact') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM contact_messages WHERE email = ? ORDER BY created_at DESC LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
View::render('pages/verify_failed', ['reason' => 'Email is already verified or not found.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $db->prepare("UPDATE contact_messages SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
|
||||
$update->execute([$code, $expiry, $row['id']]);
|
||||
ContactService::sendVerificationEmail($email, $code);
|
||||
}
|
||||
|
||||
View::render('pages/verify_success', [
|
||||
'type' => $type,
|
||||
'message' => 'We just sent you a new verification link.'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Resend verification failed: " . $e->getMessage());
|
||||
View::render('pages/verify_failed', ['reason' => 'Unexpected error occurred.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ResendVerificationController.php
|
||||
* Version: 1.4
|
||||
* Path: /app/Controllers/ResendVerificationController.php
|
||||
* Purpose: Handles verification email resends using ResendVerificationService for centralized logic.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Controllers;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
use WizdomNetworks\WizeWeb\Services\ResendVerificationService;
|
||||
|
||||
class ResendVerificationController
|
||||
{
|
||||
/**
|
||||
* @var ResendVerificationService Service that handles logic for resend rate-limiting and dispatch.
|
||||
*/
|
||||
private ResendVerificationService $resendService;
|
||||
|
||||
/**
|
||||
* Constructor to initialize ResendVerificationService.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->resendService = new ResendVerificationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a POST request to resend a verification email.
|
||||
* Validates email and type, and then delegates the resend attempt to the service.
|
||||
* Renders either a success or failure view based on outcome.
|
||||
*
|
||||
* Expects 'email' and 'type' keys to be set in $_POST.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$type = trim($_POST['type'] ?? '');
|
||||
|
||||
if (!$email || !$type || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
View::render('pages/verify_failed', ['reason' => 'Invalid email or type.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->resendService->attemptResend($type, $email);
|
||||
|
||||
if (!$result['success']) {
|
||||
View::render('pages/verify_failed', ['reason' => $result['message']]);
|
||||
} else {
|
||||
View::render('pages/verify_success', [
|
||||
'type' => $type,
|
||||
'message' => $result['message']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
namespace WizdomNetworks\WizeWeb\Controllers;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
use WizdomNetworks\WizeWeb\Utils\Database;
|
||||
use WizdomNetworks\WizeWeb\Utils\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
|
||||
class SubscriberController
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
namespace WizdomNetworks\WizeWeb\Controllers;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
use WizdomNetworks\WizeWeb\Utils\Database;
|
||||
use WizdomNetworks\WizeWeb\Utils\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
|
||||
class UnsubscribeController
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,71 +1,177 @@
|
|||
<?php
|
||||
/**
|
||||
* File: VerificationController.php
|
||||
* Version: 1.2
|
||||
* Version: 1.10
|
||||
* Path: /app/Controllers/VerificationController.php
|
||||
* Purpose: Handles email verification for newsletter and contact messages, including code expiration and attempt logging.
|
||||
* Now wired to use EmailService for all post-verification messaging, including unified contact+newsletter handling.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Controllers;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
use WizdomNetworks\WizeWeb\Utils\Database;
|
||||
use WizdomNetworks\WizeWeb\Utils\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utils\EmailHelper;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Services\EmailService;
|
||||
|
||||
class VerificationController
|
||||
{
|
||||
public function verify(): void
|
||||
private EmailService $emailService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
try {
|
||||
$code = $_GET['code'] ?? '';
|
||||
if (empty($code)) {
|
||||
Logger::error("Email verification attempted without a code.");
|
||||
View::render('pages/verify_failed', ['reason' => 'No verification code provided.']);
|
||||
return;
|
||||
}
|
||||
$this->emailService = new EmailService();
|
||||
}
|
||||
|
||||
$db = Database::getConnection();
|
||||
/**
|
||||
* Handles email verification for newsletter and contact submissions using a unique code.
|
||||
*
|
||||
* - If the code matches an unverified record: marks it as verified and sends confirmations.
|
||||
* - If already verified: shows a message.
|
||||
* - If expired: prompts user to resend.
|
||||
* - If invalid: redirects user to restart the process.
|
||||
*
|
||||
* @param string $code The verification code from the URL path.
|
||||
* @return void
|
||||
*/
|
||||
public function verify(string $code): void
|
||||
{
|
||||
try {
|
||||
if (empty($code)) {
|
||||
Logger::error("Email verification attempted without a code.");
|
||||
View::render('pages/verify_failed', [
|
||||
'reason' => 'No verification code provided.',
|
||||
'redirect' => true
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check subscribers table
|
||||
$stmt = $db->prepare("SELECT id, is_verified, email, verification_expires_at FROM subscribers WHERE verification_code = ?");
|
||||
$db = Database::getConnection();
|
||||
$subscriber = null;
|
||||
$table = null;
|
||||
$type = null;
|
||||
|
||||
// Attempt to locate the subscriber record by code in either table
|
||||
$stmt = $db->prepare("SELECT * FROM subscribers WHERE verification_code = ?");
|
||||
$stmt->execute([$code]);
|
||||
$subscriber = $stmt->fetch();
|
||||
|
||||
if ($subscriber) {
|
||||
$table = 'subscribers';
|
||||
$type = 'newsletter';
|
||||
} else {
|
||||
$stmt = $db->prepare("SELECT * FROM contact_messages WHERE verification_code = ?");
|
||||
$stmt->execute([$code]);
|
||||
$subscriber = $stmt->fetch();
|
||||
|
||||
// Log verification attempt (even if failed)
|
||||
$logAttempt = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
|
||||
$logAttempt->execute([
|
||||
$subscriber['email'] ?? '[unknown]',
|
||||
'newsletter',
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
]);
|
||||
|
||||
if ($subscriber) {
|
||||
if (!empty($subscriber['verification_expires_at']) && strtotime($subscriber['verification_expires_at']) < time()) {
|
||||
View::render('pages/verify_failed', ['reason' => 'Your verification link has expired. Please request a new one.']);
|
||||
return;
|
||||
}
|
||||
$table = 'contact_messages';
|
||||
$type = 'contact';
|
||||
}
|
||||
}
|
||||
|
||||
if ((int) $subscriber['is_verified'] === 1) {
|
||||
View::render('pages/verify_success', ['type' => 'newsletter', 'message' => 'This subscription has already been verified.']);
|
||||
return;
|
||||
}
|
||||
// If no record was found at all
|
||||
if (!$subscriber) {
|
||||
Logger::error("Invalid verification code attempted: $code");
|
||||
View::render('pages/verify_failed', [
|
||||
'reason' => 'That link is invalid. You may need to start a new submission.',
|
||||
'redirect' => true
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $db->prepare("UPDATE subscribers SET is_verified = 1, verification_code = NULL WHERE id = ?");
|
||||
$update->execute([$subscriber['id']]);
|
||||
// Handle expired code case
|
||||
if (!empty($subscriber['verification_expires_at']) && strtotime($subscriber['verification_expires_at']) < time()) {
|
||||
Logger::info("Verification link expired: $code");
|
||||
View::render('pages/verify_failed', [
|
||||
'reason' => 'Your verification link has expired. Please request a new one.',
|
||||
'type' => $type ?? 'unknown'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::info("Subscriber verified: ID " . $subscriber['id']);
|
||||
View::render('pages/verify_success', ['type' => 'newsletter']);
|
||||
return;
|
||||
// Log the verification attempt regardless of outcome
|
||||
$safeType = in_array($type, ['contact', 'newsletter', 'contact+newsletter'], true) ? $type : 'unknown';
|
||||
$logAttempt = $db->prepare("
|
||||
INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent)
|
||||
VALUES (?, ?, NOW(), ?, ?)
|
||||
");
|
||||
$logAttempt->execute([
|
||||
$subscriber['email'] ?? '[unknown]',
|
||||
$safeType,
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
]);
|
||||
|
||||
// If already verified
|
||||
if ((int) $subscriber['is_verified'] === 1) {
|
||||
View::render('pages/verify_success', [
|
||||
'type' => $type ?? 'unknown',
|
||||
'message' => 'This submission has already been verified.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the submission as verified
|
||||
$update = $db->prepare("UPDATE $table SET is_verified = 1, verification_code = NULL WHERE id = ?");
|
||||
$update->execute([$subscriber['id']]);
|
||||
|
||||
Logger::info("Subscriber verified: ID {$subscriber['id']} via $type");
|
||||
|
||||
// Handle post-verification logic for contact submissions
|
||||
if ($type === 'contact') {
|
||||
$stmt = $db->prepare("
|
||||
SELECT first_name, last_name, subject, message, pending_newsletter_opt_in
|
||||
FROM contact_messages WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$subscriber['id']]);
|
||||
$details = $stmt->fetch();
|
||||
|
||||
$emailData = [
|
||||
'email' => $subscriber['email'],
|
||||
'first_name' => $details['first_name'] ?? '',
|
||||
'last_name' => $details['last_name'] ?? '',
|
||||
'subject' => $details['subject'] ?? '',
|
||||
'message' => $details['message'] ?? '',
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
];
|
||||
|
||||
// If opted in to newsletter from contact form
|
||||
if (!empty($details['pending_newsletter_opt_in'])) {
|
||||
$this->emailService->sendContactAndNewsletterWelcome($emailData);
|
||||
|
||||
$db->prepare("UPDATE contact_messages SET pending_newsletter_opt_in = 0 WHERE id = ?")
|
||||
->execute([$subscriber['id']]);
|
||||
|
||||
$db->prepare("
|
||||
INSERT INTO subscribers (email, is_verified, created_at)
|
||||
VALUES (?, 1, NOW())
|
||||
ON DUPLICATE KEY UPDATE is_verified = 1
|
||||
")->execute([$subscriber['email']]);
|
||||
|
||||
$type = 'contact+newsletter'; // Refined to reflect both intents
|
||||
} else {
|
||||
$this->emailService->sendConfirmationToUser($emailData);
|
||||
}
|
||||
|
||||
Logger::error("Invalid or expired verification code: $code");
|
||||
View::render('pages/verify_failed', ['reason' => 'Verification code is invalid or expired.']);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Verification exception: " . $e->getMessage());
|
||||
View::render('pages/verify_failed', ['reason' => 'An error occurred during verification.']);
|
||||
$this->emailService->sendSalesNotification($emailData);
|
||||
}
|
||||
|
||||
// Final success render
|
||||
View::render('pages/verify_success', [
|
||||
'type' => $type ?? 'unknown',
|
||||
'message' => null
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Verification exception: " . $e->getMessage());
|
||||
View::render('pages/verify_failed', [
|
||||
'reason' => 'An error occurred during verification.',
|
||||
'redirect' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ContactModel.php
|
||||
* Version: 2.1
|
||||
* Version: 2.2
|
||||
* Path: /app/Models/ContactModel.php
|
||||
* Purpose: Manages saving and retrieving contact records from both legacy and full form submissions.
|
||||
* Purpose: Manages saving and retrieving contact records from both legacy and full form submissions, including newsletter opt-in tracking.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
|
|
@ -37,10 +37,7 @@ class ContactModel
|
|||
public function addContact(array $contactData): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO contacts (name, email, message)
|
||||
VALUES (:name, :email, :message)
|
||||
");
|
||||
$stmt = $this->db->prepare("INSERT INTO contacts (name, email, message) VALUES (:name, :email, :message)");
|
||||
|
||||
$name = trim(($contactData['name'] ?? '') ?: (($contactData['first_name'] ?? '') . ' ' . ($contactData['last_name'] ?? '')));
|
||||
$stmt->bindParam(':name', $name);
|
||||
|
|
@ -57,6 +54,7 @@ class ContactModel
|
|||
|
||||
/**
|
||||
* Saves full contact form submission to the `contact_messages` table.
|
||||
* Includes newsletter opt-in flag.
|
||||
*
|
||||
* @param array $formData Associative array of form input
|
||||
* @return bool True on success, false on failure
|
||||
|
|
@ -64,15 +62,13 @@ class ContactModel
|
|||
public function saveContactForm(array $formData): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO contact_messages (
|
||||
$stmt = $this->db->prepare("INSERT INTO contact_messages (
|
||||
first_name, last_name, email, phone, subject, message,
|
||||
ip_address, user_agent
|
||||
ip_address, user_agent, pending_newsletter_opt_in
|
||||
) VALUES (
|
||||
:first_name, :last_name, :email, :phone, :subject, :message,
|
||||
:ip_address, :user_agent
|
||||
)
|
||||
");
|
||||
:ip_address, :user_agent, :pending_newsletter_opt_in
|
||||
)");
|
||||
|
||||
$stmt->bindParam(':first_name', $formData['first_name']);
|
||||
$stmt->bindParam(':last_name', $formData['last_name']);
|
||||
|
|
@ -83,6 +79,9 @@ class ContactModel
|
|||
$stmt->bindParam(':ip_address', $formData['ip_address']);
|
||||
$stmt->bindParam(':user_agent', $formData['user_agent']);
|
||||
|
||||
$newsletterOptIn = $formData['pending_newsletter_opt_in'] ?? 0;
|
||||
$stmt->bindParam(':pending_newsletter_opt_in', $newsletterOptIn);
|
||||
|
||||
return $stmt->execute();
|
||||
} catch (Exception $e) {
|
||||
Logger::error("ContactModel::saveContactForm failed: " . $e->getMessage());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
/**
|
||||
* File: EmailService.php
|
||||
* Version: 1.4
|
||||
* Path: /app/Services/EmailService.php
|
||||
* Purpose: Centralized service for composing and sending all application emails including contact, newsletter, and system notifications.
|
||||
* Includes support for unified contact + newsletter welcome messages.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\EmailHelper;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Services\VerificationService;
|
||||
|
||||
class EmailService
|
||||
{
|
||||
private const TEMPLATE_VERIFICATION_CONTACT = 'verify_contact';
|
||||
private const TEMPLATE_VERIFICATION_NEWSLETTER = 'verify_newsletter';
|
||||
private const TEMPLATE_CONFIRMATION_CONTACT = 'verified_confirmation';
|
||||
private const TEMPLATE_SALES_ALERT = 'sales_lead_alert';
|
||||
private const TEMPLATE_CONTACT_NEWSLETTER = 'contact_and_newsletter';
|
||||
private const TABLE_SUBSCRIBERS = 'subscribers';
|
||||
|
||||
private VerificationService $verificationService;
|
||||
|
||||
/**
|
||||
* Initializes the email service and loads the verification code service dependency.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->verificationService = new VerificationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a verification email using the specified template and context.
|
||||
*
|
||||
* @param string $email Recipient email address
|
||||
* @param string $code Verification code
|
||||
* @param string $template Email template to render
|
||||
* @param array $context Template variables to inject
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function sendVerificationEmail(string $email, string $code, string $template, array $context = []): bool
|
||||
{
|
||||
$context['verification_link'] = rtrim($_ENV['APP_URL'], '/') . "/verify/" . $code;
|
||||
$body = EmailHelper::renderTemplate($template, $context);
|
||||
$subject = 'Please verify your email';
|
||||
|
||||
return EmailHelper::send($email, $subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new or existing newsletter subscription and sends a verification email.
|
||||
*
|
||||
* @param string $email User's email address
|
||||
* @param string $ip User's IP address
|
||||
* @param string $userAgent User agent string
|
||||
* @return bool True if verification email sent, false otherwise
|
||||
*/
|
||||
public function subscribeNewsletter(string $email, string $ip, string $userAgent): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
|
||||
$stmt = $db->prepare("SELECT is_verified FROM subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if ($row && (int)$row['is_verified'] === 1) {
|
||||
Logger::info("Newsletter signup skipped (already verified): $email");
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = $this->verificationService->generateCode();
|
||||
$expiresAt = $this->verificationService->getExpirationTime();
|
||||
|
||||
if ($row) {
|
||||
$stmt = $db->prepare("UPDATE subscribers SET verification_code = ?, ip_address = ?, user_agent = ?, created_at = NOW() WHERE email = ?");
|
||||
$stmt->execute([$code, $ip, $userAgent, $email]);
|
||||
} else {
|
||||
$stmt = $db->prepare("INSERT INTO subscribers (email, verification_code, is_verified, ip_address, user_agent, created_at) VALUES (?, ?, 0, ?, ?, NOW())");
|
||||
$stmt->execute([$email, $code, $ip, $userAgent]);
|
||||
}
|
||||
|
||||
Logger::info("Newsletter subscription initiated for $email, verification code generated.");
|
||||
|
||||
return $this->sendVerificationEmail($email, $code, self::TEMPLATE_VERIFICATION_NEWSLETTER);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Newsletter subscription failed for $email: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a confirmation email to a contact form submitter after successful verification.
|
||||
*
|
||||
* @param array $data Associative array containing user data and message details
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function sendConfirmationToUser(array $data): bool
|
||||
{
|
||||
$body = EmailHelper::renderTemplate(self::TEMPLATE_CONFIRMATION_CONTACT, $data);
|
||||
$subject = 'Your Email is Verified – Wizdom Networks';
|
||||
|
||||
return EmailHelper::send($data['email'], $subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the internal sales team when a new contact form submission is received.
|
||||
*
|
||||
* @param array $data The contact form data
|
||||
* @return bool True if at least one email sent, false otherwise
|
||||
*/
|
||||
public function sendSalesNotification(array $data): bool
|
||||
{
|
||||
$recipients = $_ENV['SALES_EMAILS'] ?? '';
|
||||
if (empty($recipients)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = EmailHelper::renderTemplate(self::TEMPLATE_SALES_ALERT, $data);
|
||||
$subject = 'New Contact Form Submission';
|
||||
|
||||
foreach (explode(',', $recipients) as $email) {
|
||||
$trimmed = trim($email);
|
||||
if (!empty($trimmed)) {
|
||||
EmailHelper::send($trimmed, $subject, $body);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a unified welcome email when a user both contacts us and subscribes to the newsletter.
|
||||
*
|
||||
* @param array $data Associative array containing contact form fields and metadata
|
||||
* @return bool True on successful send, false otherwise
|
||||
*/
|
||||
public function sendContactAndNewsletterWelcome(array $data): bool
|
||||
{
|
||||
$body = EmailHelper::renderTemplate(self::TEMPLATE_CONTACT_NEWSLETTER, $data);
|
||||
$subject = 'Thanks for reaching out – and welcome!';
|
||||
|
||||
return EmailHelper::send($data['email'], $subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a system alert to configured admin recipients.
|
||||
*
|
||||
* @param string $context Description of the error context or origin
|
||||
* @param string $errorMessage The error message or exception
|
||||
* @param array|string $data Optional contextual data to include in the alert
|
||||
* @return void
|
||||
*/
|
||||
public function alertAdmins(string $context, string $errorMessage, $data = []): void
|
||||
{
|
||||
EmailHelper::alertAdmins($context, $errorMessage, $data);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utils\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Utils\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception as MailException;
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ class NewsletterService
|
|||
private static function sendVerificationEmail(string $email, string $code): bool
|
||||
{
|
||||
try {
|
||||
$verifyUrl = $_ENV['BASE_URL'] . "/verify?code=" . urlencode($code);
|
||||
$verifyUrl = $_ENV['APP_URL'] . "/verify?code=" . urlencode($code);
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ResendVerificationService.php
|
||||
* Version: 1.4
|
||||
* Path: /app/Services/ResendVerificationService.php
|
||||
* Purpose: Encapsulates logic for validating, logging, and processing verification email resends.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Services\EmailService;
|
||||
use WizdomNetworks\WizeWeb\Services\VerificationService;
|
||||
|
||||
class ResendVerificationService
|
||||
{
|
||||
/**
|
||||
* @var EmailService Handles email composition and delivery.
|
||||
*/
|
||||
private EmailService $emailService;
|
||||
|
||||
/**
|
||||
* @var VerificationService Handles generation and storage of verification codes.
|
||||
*/
|
||||
private VerificationService $verificationService;
|
||||
|
||||
/**
|
||||
* Constructor initializes email and verification services.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->emailService = new EmailService();
|
||||
$this->verificationService = new VerificationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resend a verification email for a given type and address.
|
||||
* Performs rate limiting checks and logs the attempt if permitted.
|
||||
* Generates and assigns a new verification code and triggers an email send.
|
||||
*
|
||||
* @param string $type Either 'contact' or 'newsletter'
|
||||
* @param string $email Email address to resend to
|
||||
* @return array ['success' => bool, 'message' => string] Outcome and message for user feedback
|
||||
*/
|
||||
public function attemptResend(string $type, string $email): array
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
|
||||
// Rate limit: no more than 3 per day
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 1 DAY");
|
||||
$stmt->execute([$email, $type]);
|
||||
if ((int)$stmt->fetchColumn() >= 3) {
|
||||
return ['success' => false, 'message' => 'You have reached the daily resend limit. Please try again tomorrow.'];
|
||||
}
|
||||
|
||||
// Rate limit: no more than 1 every 5 minutes
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 5 MINUTE");
|
||||
$stmt->execute([$email, $type]);
|
||||
if ((int)$stmt->fetchColumn() > 0) {
|
||||
return ['success' => false, 'message' => 'You must wait a few minutes before requesting another verification email.'];
|
||||
}
|
||||
|
||||
// Log attempt
|
||||
$stmt = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
|
||||
$stmt->execute([
|
||||
$email,
|
||||
$type,
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
$code = $this->verificationService->generateCode();
|
||||
$expiry = $this->verificationService->getExpirationTime();
|
||||
|
||||
if ($type === 'newsletter') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
return ['success' => false, 'message' => 'Email is already verified or not found.'];
|
||||
}
|
||||
|
||||
$this->verificationService->assignCodeToRecord('subscribers', $row['id'], $code, $expiry);
|
||||
$this->emailService->sendVerificationEmail($email, $code, 'verify_newsletter');
|
||||
} elseif ($type === 'contact') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM contact_messages WHERE email = ? ORDER BY created_at DESC LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
return ['success' => false, 'message' => 'Email is already verified or not found.'];
|
||||
}
|
||||
|
||||
$this->verificationService->assignCodeToRecord('contact_messages', $row['id'], $code, $expiry);
|
||||
$this->emailService->sendVerificationEmail($email, $code, 'verify_contact');
|
||||
} else {
|
||||
return ['success' => false, 'message' => 'Invalid verification type specified.'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'We just sent you a new verification link.'];
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("ResendVerificationService::attemptResend exception: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => 'An unexpected error occurred.'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
/**
|
||||
* File: VerificationService.php
|
||||
* Version: 1.0
|
||||
* Path: /app/Services/VerificationService.php
|
||||
* Purpose: Manages generation, storage, expiration, and removal of email verification codes.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
class VerificationService
|
||||
{
|
||||
private const CODE_BYTES = 16;
|
||||
private const EXPIRATION_INTERVAL = '+72 hours';
|
||||
|
||||
/**
|
||||
* Generates a secure verification code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateCode(): string
|
||||
{
|
||||
return bin2hex(random_bytes(self::CODE_BYTES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration timestamp for a verification code.
|
||||
*
|
||||
* @return string MySQL-compatible datetime string
|
||||
*/
|
||||
public function getExpirationTime(): string
|
||||
{
|
||||
return (new DateTime(self::EXPIRATION_INTERVAL))->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a verification code to a contact or subscriber record.
|
||||
*
|
||||
* @param string $table Table name (e.g., 'subscribers', 'contact_messages')
|
||||
* @param int $id Record ID
|
||||
* @param string $code Verification code
|
||||
* @param string $expiresAt Expiration timestamp
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function assignCodeToRecord(string $table, int $id, string $code, string $expiresAt): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
|
||||
return $stmt->execute([$code, $expiresAt, $id]);
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to assign verification code to {$table} ID {$id}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes expired verification codes from a table.
|
||||
*
|
||||
* @param string $table Table name (e.g., 'subscribers', 'contact_messages')
|
||||
* @return int Number of rows deleted
|
||||
*/
|
||||
public function deleteExpiredCodes(string $table): int
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE verification_expires_at IS NOT NULL AND verification_expires_at < NOW()");
|
||||
$stmt->execute();
|
||||
return $stmt->rowCount();
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to clear expired codes in {$table}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the verification code from a specific record.
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int $id Record ID
|
||||
* @return bool
|
||||
*/
|
||||
public function clearCode(string $table, int $id): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to clear verification code for {$table} ID {$id}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
/**
|
||||
* File: EmailHelper.php
|
||||
* Version: 2.5
|
||||
* Version: 2.10
|
||||
* Path: /app/Utilities/EmailHelper.php
|
||||
* Purpose: Sends contact confirmations, sales notifications, and admin alerts with proper formatting and logic.
|
||||
* Purpose: Low-level utility for PHPMailer configuration, rendering, and transport of outbound email.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
|
|
@ -14,6 +14,12 @@ use PHPMailer\PHPMailer\Exception;
|
|||
|
||||
class EmailHelper
|
||||
{
|
||||
/**
|
||||
* Configures PHPMailer with environment settings.
|
||||
*
|
||||
* @param PHPMailer $mail
|
||||
* @return void
|
||||
*/
|
||||
public static function configureMailer(PHPMailer $mail): void
|
||||
{
|
||||
$mail->isSMTP();
|
||||
|
|
@ -48,6 +54,12 @@ class EmailHelper
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of emails and returns an array of valid addresses.
|
||||
*
|
||||
* @param string $rawList
|
||||
* @return array
|
||||
*/
|
||||
private static function parseRecipients(string $rawList): array
|
||||
{
|
||||
$emails = explode(',', $rawList);
|
||||
|
|
@ -63,173 +75,83 @@ class EmailHelper
|
|||
return $validEmails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a basic HTML email.
|
||||
*
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $body
|
||||
* @return bool
|
||||
*/
|
||||
public static function send(string $to, string $subject, string $body): bool
|
||||
{
|
||||
try {
|
||||
$mail = self::getMailer();
|
||||
$mail->addAddress($to);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $body;
|
||||
$mail->isHTML(true);
|
||||
|
||||
return $mail->send();
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Email send failed to $to: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function buildContactHtmlBody(array $data): string
|
||||
{
|
||||
return "
|
||||
<strong>Name:</strong> {$data['first_name']} {$data['last_name']}<br>
|
||||
<strong>Email:</strong> {$data['email']}<br>
|
||||
<strong>Phone:</strong> {$data['phone']}<br>
|
||||
<strong>Subject:</strong> {$data['subject']}<br>
|
||||
<strong>Message:</strong><br>
|
||||
<pre style='white-space:pre-wrap;'>{$data['message']}</pre>
|
||||
";
|
||||
}
|
||||
|
||||
private static function buildErrorReportHtml(string $context, string $errorMessage, $data = []): string
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
$data = is_array($decoded) ? $decoded : ['raw_data' => $data];
|
||||
}
|
||||
|
||||
$body = "
|
||||
<strong>Context:</strong> {$context}<br>
|
||||
<strong>Error Message:</strong><br>
|
||||
<pre style='white-space:pre-wrap;color:#8b0000;'>{$errorMessage}</pre>
|
||||
";
|
||||
|
||||
if (!empty($data)) {
|
||||
$body .= "<hr><strong>Associated Data:</strong><br><ul>";
|
||||
foreach ($data as $key => $value) {
|
||||
$safeKey = htmlspecialchars($key);
|
||||
$safeValue = nl2br(htmlspecialchars((string)$value));
|
||||
$body .= "<li><strong>{$safeKey}:</strong> {$safeValue}</li>";
|
||||
}
|
||||
$body .= "</ul>";
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private static function buildSalesHtmlBody(array $data): string
|
||||
{
|
||||
$submittedAt = date('Y-m-d H:i:s');
|
||||
return "
|
||||
<p><strong>New contact submission received</strong> on {$submittedAt}.</p>
|
||||
<ul>
|
||||
<li><strong>Name:</strong> {$data['first_name']} {$data['last_name']}</li>
|
||||
<li><strong>Email:</strong> {$data['email']}</li>
|
||||
<li><strong>Phone:</strong> {$data['phone']}</li>
|
||||
<li><strong>Subject:</strong> {$data['subject']}</li>
|
||||
<li><strong>Message:</strong><br><pre style='white-space:pre-wrap;'>{$data['message']}</pre></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p style='font-size: 0.9em; color: #888;'>IP: {$data['ip_address']}<br>User-Agent: {$data['user_agent']}</p>
|
||||
";
|
||||
}
|
||||
|
||||
private static function buildConfirmationHtmlBody(array $data): string
|
||||
{
|
||||
$submittedAt = date('Y-m-d H:i:s');
|
||||
return "
|
||||
<p>Hi {$data['first_name']},</p>
|
||||
<p>Thank you for contacting Wizdom Networks. This message confirms that we received your inquiry on <strong>{$submittedAt}</strong>.</p>
|
||||
<ul>
|
||||
<li><strong>Subject:</strong> {$data['subject']}</li>
|
||||
<li><strong>Message:</strong><br><pre style='white-space:pre-wrap;'>{$data['message']}</pre></li>
|
||||
</ul>
|
||||
<p>We’ll be in touch shortly. If it’s urgent, call us at <strong>416-USE-WISE</strong>.</p>
|
||||
<p style='font-size: 0.9em; color: #888;'>IP: {$data['ip_address']}<br>User-Agent: {$data['user_agent']}</p>
|
||||
";
|
||||
}
|
||||
|
||||
public static function sendContactNotification(array $data): bool
|
||||
{
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
self::configureMailer($mail);
|
||||
|
||||
$recipients = self::parseRecipients($_ENV['SALES_EMAILS'] ?? '');
|
||||
foreach ($recipients as $email) {
|
||||
$mail->addAddress($email);
|
||||
}
|
||||
|
||||
if (empty($mail->getToAddresses())) {
|
||||
Logger::error("EmailHelper: No valid SALES_EMAILS configured.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$mail->addAddress($to);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $body;
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = 'New Contact Form Submission';
|
||||
$mail->Body = self::buildSalesHtmlBody($data);
|
||||
|
||||
$mail->send();
|
||||
Logger::info("Email sent successfully to $to with subject: $subject");
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Email send failure to sales: " . $e->getMessage());
|
||||
self::alertAdmins("sendContactNotification", $e->getMessage(), $data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function sendConfirmationToUser(array $data): bool
|
||||
{
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
self::configureMailer($mail);
|
||||
|
||||
$mail->addAddress($data['email'], "{$data['first_name']} {$data['last_name']}");
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = 'Your Wizdom Networks Contact Form Submission';
|
||||
$mail->Body = self::buildConfirmationHtmlBody($data);
|
||||
|
||||
$mail->send();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Email send failure to user: " . $e->getMessage());
|
||||
self::alertAdmins("sendConfirmationToUser", $e->getMessage(), $data);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Email send failed to $to: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a system alert to ADMIN_EMAILS with error context and message.
|
||||
*
|
||||
* @param string $context
|
||||
* @param string $errorMessage
|
||||
* @param array|string $data Data array or JSON string for the report
|
||||
* @return void
|
||||
*/
|
||||
public static function alertAdmins(string $context, string $errorMessage, $data = []): void
|
||||
* Sends a system-level alert email to admins using an HTML template.
|
||||
*
|
||||
* @param string $context
|
||||
* @param string $errorMessage
|
||||
* @param array|string $data
|
||||
* @return void
|
||||
*/
|
||||
public static function alertAdmins(string $context, string $errorMessage, $data = []): void
|
||||
{
|
||||
$recipients = self::parseRecipients($_ENV['ADMIN_EMAILS'] ?? '');
|
||||
if (empty($recipients)) {
|
||||
Logger::error("EmailHelper: No valid ADMIN_EMAILS configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlBody = self::renderTemplate('system_alert', [
|
||||
'context' => $context,
|
||||
'errorMessage' => $errorMessage,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
foreach ($recipients as $email) {
|
||||
self::send($email, "[System Alert] Error in {$context}", $htmlBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an email template with dynamic variables.
|
||||
*
|
||||
* @param string $templateName
|
||||
* @param array $vars
|
||||
* @return string
|
||||
*/
|
||||
public static function renderTemplate(string $templateName, array $vars = []): string
|
||||
{
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
self::configureMailer($mail);
|
||||
|
||||
$recipients = self::parseRecipients($_ENV['ADMIN_EMAILS'] ?? '');
|
||||
foreach ($recipients as $email) {
|
||||
$mail->addAddress($email);
|
||||
$templatePath = __DIR__ . '/../../resources/views/emails/' . $templateName . '.php';
|
||||
if (!file_exists($templatePath)) {
|
||||
throw new \Exception("Template not found: $templateName");
|
||||
}
|
||||
|
||||
if (empty($mail->getToAddresses())) {
|
||||
Logger::error("EmailHelper: No valid ADMIN_EMAILS configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = "[System Alert] Error in {$context}";
|
||||
$mail->Body = self::buildErrorReportHtml($context, $errorMessage, $data);
|
||||
|
||||
$mail->send();
|
||||
} catch (Exception $e) {
|
||||
Logger::error("EmailHelper::alertAdmins failed: " . $e->getMessage());
|
||||
extract($vars);
|
||||
ob_start();
|
||||
include $templatePath;
|
||||
return ob_get_clean();
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Failed to render email template: $templateName - " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,393 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Utilities;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use PHPMAILER\PHPMAILER\SMTP;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
|
||||
class EmailUtility
|
||||
{
|
||||
private static QueueUtility $queueUtility;
|
||||
|
||||
/**
|
||||
* Initialize the EmailUtility with the QueueUtility instance.
|
||||
*
|
||||
* @param QueueUtility $queueUtility The queue utility instance for managing email queues.
|
||||
*/
|
||||
public static function initialize(QueueUtility $queueUtility): void
|
||||
{
|
||||
self::$queueUtility = $queueUtility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and validate email configuration from environment variables.
|
||||
*
|
||||
* @return array The validated email configuration settings.
|
||||
* @throws \RuntimeException If required email configurations are missing.
|
||||
*/
|
||||
private static function getConfig(): array
|
||||
{
|
||||
|
||||
if ( $_ENV['SMTP_AUTH'] === false ) {
|
||||
|
||||
|
||||
$config = [
|
||||
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
|
||||
'port' => $_ENV['SMTP_PORT'] ?? '25',
|
||||
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
|
||||
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
|
||||
'auth' => false
|
||||
];
|
||||
|
||||
foreach (['host', 'port', 'from_email'] as $field) {
|
||||
if (empty($config[$field])) {
|
||||
Logger::logError("Missing email configuration: $field");
|
||||
throw new \RuntimeException("Missing email configuration: $field");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
$config = [
|
||||
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
|
||||
'username' => $_ENV['SMTP_USERNAME'] ?? null,
|
||||
'password' => $_ENV['SMTP_PASSWORD'] ?? null,
|
||||
'port' => $_ENV['SMTP_PORT'] ?? '25',
|
||||
'encryption' => $_ENV['SMTP_ENCRYPTION'] ?? 'none',
|
||||
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
|
||||
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
|
||||
'smtpsecure' => $_ENV['SMTP_AUTH'],
|
||||
'auth' => true,
|
||||
'autotls' => true
|
||||
];
|
||||
|
||||
foreach (['host', 'port', 'username', 'password', 'from_email'] as $field) {
|
||||
if (empty($config[$field])) {
|
||||
Logger::logError("Missing email configuration: $field");
|
||||
throw new \RuntimeException("Missing email configuration: $field");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an email template with dynamic data.
|
||||
*
|
||||
* @param string $templatePath The path to the email template file.
|
||||
* @param array $data Key-value pairs for template placeholders.
|
||||
* @return string The rendered email content.
|
||||
*/
|
||||
public static function renderTemplate(string $templatePath, array $data): string
|
||||
{
|
||||
if (!file_exists($templatePath)) {
|
||||
Logger::logError("Email template not found: $templatePath");
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = file_get_contents($templatePath);
|
||||
foreach ($data as $key => $value) {
|
||||
$content = str_replace("{{{$key}}}", $value, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log email status into the database.
|
||||
*
|
||||
* @param string $recipient The recipient email address.
|
||||
* @param string $status The status of the email (e.g., 'queued', 'sent', 'failed').
|
||||
* @param string|null $errorMessage An optional error message.
|
||||
* @return void
|
||||
*/
|
||||
private static function logEmailStatus(string $recipient, string $status, ?string $errorMessage = null): void
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$query = "INSERT INTO email_status (recipient, status, error_message, created_at) VALUES (:recipient, :status, :error_message, NOW())";
|
||||
$params = [
|
||||
':recipient' => $recipient,
|
||||
':status' => $status,
|
||||
':error_message' => $errorMessage,
|
||||
];
|
||||
$db->executeQuery($query, $params);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::logError("Failed to log email status: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify admin or sales team via email.
|
||||
*
|
||||
* @param string $emailType The type of notification (e.g., 'admin', 'sales').
|
||||
* @param string $subject The email subject.
|
||||
* @param string $templatePath Path to the notification template.
|
||||
* @param array $templateData Data for the template placeholders.
|
||||
* @return void
|
||||
*/
|
||||
public static function notifyTeam(string $emailType, string $subject, string $templatePath, array $templateData): void
|
||||
{
|
||||
$recipients = $emailType === 'admin' ? explode(',', $_ENV['ADMIN_EMAILS']) : explode(',', $_ENV['SALES_EMAILS']);
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$recipient = trim($recipient);
|
||||
if (!self::sendEmail($recipient, $subject, $templatePath, $templateData)) {
|
||||
Logger::logError("Failed to send $emailType notification to: $recipient");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email with enhanced error categorization.
|
||||
*
|
||||
* @param string $recipient Recipient email address.
|
||||
* @param string $subject Email subject.
|
||||
* @param string $templatePath Path to the email template.
|
||||
* @param array $templateData Data to replace placeholders in the template.
|
||||
* @param array $options Optional configurations (e.g., CC, BCC).
|
||||
* @param int $retryLimit The maximum number of retries for transient failures.
|
||||
* @return bool Returns true on success, false otherwise.
|
||||
*/
|
||||
public static function sendEmail(string $recipient, string $subject, string $templatePath, array $templateData = [], array $options = [], int $retryLimit = 3): bool
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
$config = self::getConfig();
|
||||
$retryCount = 0;
|
||||
|
||||
while ($retryCount <= $retryLimit) {
|
||||
try {
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPAuth = false;
|
||||
/* If authentication is enabled setup the connection */
|
||||
if ( $config['auth'] === 'true' ){
|
||||
$mail->SMTPAuth = $config['auth'];
|
||||
$mail->Username = $config['username'];
|
||||
$mail->Password = $config['password'];
|
||||
$mail->SMTPSecure = $config['encryption'];
|
||||
$mail->SMTPAutoTLS = $config['autotls'];
|
||||
}
|
||||
|
||||
$mail->Host = $config['host'];
|
||||
$mail->Port = $config['port'];
|
||||
/******************************
|
||||
$mail->SMTPDebug = $_ENV['APP_ENV'] === 'development' ? 2 : 0;
|
||||
/$mail->Debugoutput = function ($message, $level) {
|
||||
Logger::logInfo("SMTP Debug [$level]: $message");
|
||||
};
|
||||
*******************************/
|
||||
$mail->setFrom($config['from_email'], $config['from_name']);
|
||||
$mail->addAddress($recipient);
|
||||
|
||||
if (!empty($options['cc'])) {
|
||||
foreach ((array)$options['cc'] as $cc) {
|
||||
$mail->addCC($cc);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['bcc'])) {
|
||||
foreach ((array)$options['bcc'] as $bcc) {
|
||||
$mail->addBCC($bcc);
|
||||
}
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = self::renderTemplate($templatePath, $templateData);
|
||||
|
||||
$mail->send();
|
||||
Logger::logInfo("Email sent to $recipient with subject: $subject");
|
||||
self::logEmailStatus($recipient, 'sent');
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$retryCount++;
|
||||
$error = $mail->ErrorInfo;
|
||||
Logger::logWarning("Email send failed for $recipient (Attempt $retryCount/$retryLimit): $error");
|
||||
|
||||
if (str_contains($error, '452 4.3.1')) {
|
||||
Logger::logWarning("Transient error detected for $recipient: $error");
|
||||
} elseif (str_contains($error, '550')) {
|
||||
Logger::logError("Permanent error detected for $recipient: $error");
|
||||
self::logEmailStatus($recipient, 'failed', $error);
|
||||
return false;
|
||||
} elseif (str_contains($error, '421')) {
|
||||
Logger::logWarning("Rate-limiting error detected for $recipient: $error");
|
||||
} else {
|
||||
Logger::logError("Unhandled SMTP error for $recipient: $error");
|
||||
}
|
||||
|
||||
if (str_contains($error, '452') || str_contains($error, '421')) {
|
||||
if ($retryCount > $retryLimit) {
|
||||
Logger::logError("Exceeded retry limit for email to $recipient: $error");
|
||||
self::logEmailStatus($recipient, 'failed', $error);
|
||||
return false;
|
||||
}
|
||||
|
||||
sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::logError("Email permanently failed for $recipient: $error");
|
||||
self::logEmailStatus($recipient, 'failed', $error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the email queue and send emails in batches.
|
||||
*
|
||||
* @param int $batchSize Number of emails to process in a single batch.
|
||||
* @param int $maxRetries Maximum retry attempts for failed emails.
|
||||
* @return void
|
||||
*/
|
||||
public static function processEmailQueue(int $batchSize = 10, int $maxRetries = 3): void
|
||||
{
|
||||
for ($i = 0; $i < $batchSize; $i++) {
|
||||
$emailData = self::$queueUtility->dequeue('email');
|
||||
|
||||
if ($emailData === null) {
|
||||
Logger::logInfo("No more emails to process in the queue.");
|
||||
break;
|
||||
}
|
||||
|
||||
$success = self::sendEmail(
|
||||
$emailData['recipient'],
|
||||
$emailData['subject'],
|
||||
$emailData['templatePath'],
|
||||
$emailData['templateData'],
|
||||
$emailData['options']
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
$retries = $emailData['retries'] ?? 0;
|
||||
|
||||
if ($retries < $maxRetries) {
|
||||
$priority = $emailData['priority'] ?? 0;
|
||||
$emailData['retries'] = $retries + 1;
|
||||
self::$queueUtility->enqueue('email', $emailData, $priority);
|
||||
Logger::logWarning(
|
||||
"Email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
|
||||
);
|
||||
} else {
|
||||
Logger::logError("Email permanently failed for recipient: {$emailData['recipient']}");
|
||||
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
|
||||
}
|
||||
} else {
|
||||
self::logEmailStatus($emailData['recipient'], 'sent');
|
||||
}
|
||||
}
|
||||
|
||||
Logger::logInfo("Email queue processing completed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process contact-related email queue.
|
||||
*
|
||||
* @param int $batchSize Number of emails to process in a single batch.
|
||||
* @param int $maxRetries Maximum retry attempts for failed emails.
|
||||
* @return void
|
||||
*/
|
||||
public static function processContactQueue(int $batchSize = 10, int $maxRetries = 3): void
|
||||
{
|
||||
Logger::logInfo("Processing contact email queue...");
|
||||
|
||||
for ($i = 0; $i < $batchSize; $i++) {
|
||||
$emailData = self::$queueUtility->dequeue('contact_email');
|
||||
|
||||
if ($emailData === null) {
|
||||
Logger::logInfo("No more emails to process in the contact queue.");
|
||||
break;
|
||||
}
|
||||
|
||||
$success = self::sendEmail(
|
||||
$emailData['recipient'],
|
||||
$emailData['subject'],
|
||||
$emailData['templatePath'],
|
||||
$emailData['templateData'],
|
||||
$emailData['options']
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
$retries = $emailData['retries'] ?? 0;
|
||||
|
||||
if ($retries < $maxRetries) {
|
||||
$priority = $emailData['priority'] ?? 0;
|
||||
$emailData['retries'] = $retries + 1;
|
||||
self::$queueUtility->enqueue('contact_email', $emailData, $priority);
|
||||
Logger::logWarning(
|
||||
"Contact email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
|
||||
);
|
||||
} else {
|
||||
Logger::logError("Contact email permanently failed for recipient: {$emailData['recipient']}");
|
||||
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
|
||||
}
|
||||
} else {
|
||||
self::logEmailStatus($emailData['recipient'], 'sent');
|
||||
}
|
||||
}
|
||||
|
||||
Logger::logInfo("Contact email queue processing completed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the status of a specific email by recipient.
|
||||
*
|
||||
* @param string $recipient Email address of the recipient.
|
||||
* @return array|null The email status or null if not found.
|
||||
*/
|
||||
public static function getEmailStatus(string $recipient): ?array
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$query = "SELECT * FROM email_status WHERE recipient = :recipient ORDER BY created_at DESC LIMIT 1";
|
||||
$params = [':recipient' => $recipient];
|
||||
return $db->fetchOne($query, $params);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::logError("Failed to retrieve email status for $recipient: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the email queue.
|
||||
*
|
||||
* @param string $queueName The name of the queue to clear (default: 'email').
|
||||
* @return void
|
||||
*/
|
||||
public static function clearQueue(string $queueName = 'email'): void
|
||||
{
|
||||
Logger::logInfo("Clearing queue: $queueName");
|
||||
|
||||
try {
|
||||
self::$queueUtility->clearQueue($queueName);
|
||||
Logger::logInfo("Queue $queueName cleared successfully.");
|
||||
} catch (\Throwable $e) {
|
||||
Logger::logError("Failed to clear queue $queueName: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all queued emails in a specific queue.
|
||||
*
|
||||
* @param string $queueName The name of the queue to inspect (default: 'email').
|
||||
* @return array List of queued emails.
|
||||
*/
|
||||
public static function listQueuedEmails(string $queueName = 'email'): array
|
||||
{
|
||||
try {
|
||||
Logger::logInfo("Listing emails in queue: $queueName");
|
||||
return self::$queueUtility->listQueue($queueName);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::logError("Failed to list emails in queue $queueName: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* File: SubmissionCheck.php
|
||||
* Version: 1.1
|
||||
* Version: 1.3
|
||||
* Purpose: Helper to detect and block repeated or abusive contact form submissions
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
|
@ -27,19 +27,22 @@ class SubmissionCheck
|
|||
public static function evaluate(PDO $pdo, string $email, ?string $phone, ?string $ip): array
|
||||
{
|
||||
try {
|
||||
// MySQL cannot bind inside INTERVAL, so we inject LOOKBACK_DAYS directly
|
||||
$lookback = (int) self::LOOKBACK_DAYS;
|
||||
|
||||
$query = "
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE email = :email AND created_at >= NOW() - INTERVAL :days DAY) AS email_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE phone = :phone AND created_at >= NOW() - INTERVAL :days DAY) AS phone_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE ip_address = :ip AND created_at >= NOW() - INTERVAL :days DAY) AS ip_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE ip_address = :ip AND created_at >= NOW() - INTERVAL 1 HOUR) AS ip_hourly
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE email = :email AND created_at >= NOW() - INTERVAL $lookback DAY) AS email_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE phone = :phone AND created_at >= NOW() - INTERVAL $lookback DAY) AS phone_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE ip_address = :ip1 AND created_at >= NOW() - INTERVAL $lookback DAY) AS ip_hits,
|
||||
(SELECT COUNT(*) FROM submission_logs WHERE ip_address = :ip2 AND created_at >= NOW() - INTERVAL 1 HOUR) AS ip_hourly
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->bindValue(':email', $email);
|
||||
$stmt->bindValue(':phone', $phone);
|
||||
$stmt->bindValue(':ip', $ip);
|
||||
$stmt->bindValue(':days', self::LOOKBACK_DAYS, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':ip1', $ip);
|
||||
$stmt->bindValue(':ip2', $ip);
|
||||
$stmt->execute();
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
// File: public/assets/js/contact-form.js
|
||||
// Version: 1.2
|
||||
// Version: 1.3
|
||||
// Purpose: Handles JS-based form submission with feedback and duplicate prevention
|
||||
|
||||
|
||||
|
|
@ -48,9 +48,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: formData
|
||||
});
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
loading.style.display = 'none';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* ============================================
|
||||
* File: public/index.php
|
||||
* Version: v1.7
|
||||
* Version: v1.8
|
||||
* Purpose: Application entry point for Arsha one-pager with dynamic & closure route support
|
||||
* Project: Wizdom Networks Website
|
||||
* ============================================
|
||||
|
|
@ -61,7 +61,7 @@ $router->add('/contact', ContactController::class, 'submit', 'POST');
|
|||
|
||||
// Verification Routes
|
||||
$router->add('/verify', VerificationController::class, 'verify', 'GET'); // e.g. /verify?email=...
|
||||
$router->add('/verify/{code}', ContactController::class, 'verify', 'GET'); // e.g. /verify/abc123...
|
||||
$router->add('/verify/{code}', VerificationController::class, 'verify', 'GET'); // e.g. /verify/abc123...
|
||||
|
||||
// Resend / Newsletter / Unsubscribe
|
||||
$router->add('/resend-verification', ResendVerificationController::class, 'handle', 'POST');
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email from HelpDesk+</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
text-align: center;
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
padding: 10px 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
.email-content {
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.email-footer {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>HelpDesk+</h1>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<p>Dear {{name}},</p>
|
||||
|
||||
<p>Thank you for reaching out to HelpDesk+. We’ve received your message and will get back to you shortly. Here’s what you submitted:</p>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>Message:</strong> {{message}}</p>
|
||||
</blockquote>
|
||||
|
||||
<p>If you have any further questions or concerns, feel free to reply to this email.</p>
|
||||
|
||||
<p>Best regards,<br>The HelpDesk+ Team</p>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
© 2025 HelpDesk+. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* File: admin_contact_alert.php
|
||||
* Version: 1.0
|
||||
* Path: /resources/views/emails/admin_contact_alert.php
|
||||
* Purpose: Notification email to admins for new contact form submissions.
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<p><strong>New Contact Form Submission Received</strong></p>
|
||||
|
||||
<p><strong>Name:</strong> <?= htmlspecialchars($first_name . ' ' . $last_name) ?><br>
|
||||
<strong>Email:</strong> <?= htmlspecialchars($email) ?><br>
|
||||
<strong>IP Address:</strong> <?= htmlspecialchars($ip_address) ?><br>
|
||||
<strong>User Agent:</strong> <?= htmlspecialchars($user_agent) ?></p>
|
||||
|
||||
<p><strong>Message:</strong><br><?= nl2br(htmlspecialchars($message)) ?></p>
|
||||
|
||||
<?php $body = ob_get_clean(); ?>
|
||||
|
||||
<?php include __DIR__ . '/../layouts/email_layout.php'; ?>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
/**
|
||||
* File: contact_and_newsletter.php
|
||||
* Path: /resources/views/emails/contact_and_newsletter.php
|
||||
* Template Name: Contact + Newsletter Confirmation
|
||||
* Purpose: Unified email for users who submitted a contact message and opted into the newsletter.
|
||||
* Triggered by: EmailService::sendContactAndNewsletterWelcome()
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
/** @var string $first_name */
|
||||
/** @var string $subject */
|
||||
/** @var string $message */
|
||||
/** @var string $ip_address */
|
||||
/** @var string $user_agent */
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thanks for Contacting Us & Welcome</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f9f9f9; color: #333; padding: 30px; }
|
||||
.container { background: #fff; border: 1px solid #ddd; padding: 25px; max-width: 640px; margin: auto; border-radius: 6px; }
|
||||
h1 { color: #005baa; }
|
||||
p, li { font-size: 1rem; line-height: 1.6; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
.footer { font-size: 0.85em; color: #777; margin-top: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Thanks for reaching out<?= isset($first_name) ? ", $first_name" : '' ?>!</h1>
|
||||
|
||||
<p>We've received your message and appreciate you taking the time to contact Wizdom Networks. A member of our team will follow up shortly.</p>
|
||||
|
||||
<p><strong>Here’s what you submitted:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Subject:</strong> <?= htmlspecialchars($subject) ?></li>
|
||||
<li><strong>Message:</strong><br><pre style="white-space:pre-wrap;"><?= htmlspecialchars($message) ?></pre></li>
|
||||
</ul>
|
||||
|
||||
<p>You also signed up for our newsletter — welcome aboard! We’ll occasionally send you updates, tips, and insights straight from our team.</p>
|
||||
|
||||
<p><strong>What to expect:</strong></p>
|
||||
<ul>
|
||||
<li>✅ A response to your inquiry</li>
|
||||
<li>✅ Occasional email updates and insights</li>
|
||||
<li>✅ You can unsubscribe at any time</li>
|
||||
</ul>
|
||||
|
||||
<p class="footer">
|
||||
IP: <?= htmlspecialchars($ip_address) ?><br>
|
||||
User-Agent: <?= htmlspecialchars($user_agent) ?>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
/**
|
||||
* File: newsletter_welcome.php
|
||||
* Template Name: Newsletter Welcome
|
||||
* Purpose: HTML email template shown to users after newsletter verification.
|
||||
* Triggered By: EmailService::sendNewsletterWelcome()
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
/** @var string $first_name */ ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Wizdom Networks</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
padding: 40px;
|
||||
}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 30px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
color: #005baa;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 0.85em;
|
||||
color: #777;
|
||||
}
|
||||
.cta {
|
||||
display: inline-block;
|
||||
background-color: #005baa;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
margin-top: 20px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cta:hover {
|
||||
background-color: #00498a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to Wizdom Networks<?= isset($first_name) ? ", $first_name" : '' ?>!</h1>
|
||||
|
||||
<p>Thanks for subscribing to the Wizdom Networks newsletter. We're glad to have you with us.</p>
|
||||
|
||||
<p>You can expect expert tips, behind-the-scenes insights, and smart strategies that help make technology work for your business—not the other way around.</p>
|
||||
|
||||
<p>If you ever want to update your preferences or tell us more about yourself, just click below.</p>
|
||||
|
||||
<a class="cta" href="https://wizdom.ca/preferences">Update Preferences</a>
|
||||
|
||||
<p class="footer">
|
||||
You're receiving this email because you opted in at wizdom.ca. If this was a mistake, you can safely <a href="https://wizdom.ca/unsubscribe">unsubscribe here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
<?php
|
||||
/**
|
||||
* File: resend_verification.php
|
||||
* Version: 1.0
|
||||
* Path: /resources/views/emails/resend_verification.php
|
||||
* Purpose: Content block for re-sent verification emails.
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<p>Hello <?= htmlspecialchars($first_name ?? 'there') ?>,</p>
|
||||
|
||||
<p>You recently requested to re-send your email verification link. Please click below to verify your email address:</p>
|
||||
|
||||
<p><a href="<?= htmlspecialchars($verification_link) ?>">Verify My Email</a></p>
|
||||
|
||||
<p>If you didn't request this, no further action is needed.</p>
|
||||
|
||||
<p>— The Wizdom Networks Team</p>
|
||||
<?php $body = ob_get_clean(); ?>
|
||||
|
||||
<?php include __DIR__ . '/../layouts/email_layout.php'; ?>
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
/**
|
||||
* File: sales_lead_alert.php
|
||||
* Version: 1.0
|
||||
* Path: /resources/views/emails/sales_lead_alert.php
|
||||
* Purpose: Notification email to sales team for new lead submissions.
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<p><strong>New Sales Lead Submitted</strong></p>
|
||||
|
||||
<p><strong>Name:</strong> <?= htmlspecialchars($first_name . ' ' . $last_name) ?><br>
|
||||
<strong>Email:</strong> <?= htmlspecialchars($email) ?><br>
|
||||
<?php if (!empty($phone)): ?><strong>Phone:</strong> <?= htmlspecialchars($phone) ?><br><?php endif; ?></p>
|
||||
|
||||
<p><strong>Message:</strong><br><?= nl2br(htmlspecialchars($message)) ?></p>
|
||||
|
||||
<?php $body = ob_get_clean(); ?>
|
||||
|
||||
<?php include __DIR__ . '/../layouts/email_layout.php'; ?>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
/**
|
||||
* File: system_alert.php
|
||||
* Path: /resources/views/emails/system_alert.php
|
||||
* Template Name: System Alert Email
|
||||
* Purpose: Sent to admins when a critical error or exception occurs.
|
||||
* Triggered By: EmailHelper::alertAdmins()
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
/** @var string $context */
|
||||
/** @var string $errorMessage */
|
||||
/** @var array|string $data */
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System Alert</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f9f9f9; color: #333; padding: 30px; }
|
||||
.container { background: #fff; border: 1px solid #ddd; padding: 25px; max-width: 720px; margin: auto; border-radius: 6px; }
|
||||
h1 { color: #d32f2f; }
|
||||
pre { background: #f1f1f1; padding: 12px; border-left: 4px solid #ccc; overflow-x: auto; }
|
||||
ul { margin-top: 0; }
|
||||
li { margin-bottom: 8px; }
|
||||
.footer { font-size: 0.85em; color: #777; margin-top: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚨 System Alert: <?= htmlspecialchars($context ?? 'Unknown') ?></h1>
|
||||
|
||||
<p><strong>Error Message:</strong></p>
|
||||
<pre><?= htmlspecialchars($errorMessage ?? 'N/A') ?></pre>
|
||||
|
||||
<?php if (!empty($data)): ?>
|
||||
<p><strong>Contextual Data:</strong></p>
|
||||
<ul>
|
||||
<?php if (is_array($data)): ?>
|
||||
<?php foreach ($data as $key => $value): ?>
|
||||
<li><strong><?= htmlspecialchars($key) ?>:</strong> <?= nl2br(htmlspecialchars((string)$value)) ?></li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li><?= nl2br(htmlspecialchars($data)) ?></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="footer">
|
||||
This alert was generated by the Wizdom Networks website infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/**
|
||||
* File: verified_confirmation.php
|
||||
* Version: 1.1
|
||||
* Path: /resources/views/emails/verified_confirmation.php
|
||||
* Purpose: Sent to user after successful email verification.
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<p>Hello <?= htmlspecialchars($first_name ?? 'there') ?>,</p>
|
||||
|
||||
<p>Your email has been successfully verified. Thank you for connecting with Wizdom Networks.</p>
|
||||
|
||||
<p>Here's a summary of your submission:</p>
|
||||
<ul>
|
||||
<?php if (!empty($last_name)): ?><li><strong>Name:</strong> <?= htmlspecialchars($first_name . ' ' . $last_name) ?></li><?php endif; ?>
|
||||
<?php if (!empty($email)): ?><li><strong>Email:</strong> <?= htmlspecialchars($email) ?></li><?php endif; ?>
|
||||
<?php if (!empty($message)): ?><li><strong>Message:</strong><br><?= nl2br(htmlspecialchars($message)) ?></li><?php endif; ?>
|
||||
</ul>
|
||||
|
||||
<p>We’ll be in touch soon if your message requires a response. In the meantime, feel free to reach out at <strong>416-USE-WISE</strong> if you need immediate assistance.</p>
|
||||
|
||||
<p>— The Wizdom Networks Team</p>
|
||||
<?php $body = ob_get_clean(); ?>
|
||||
|
||||
<?php include __DIR__ . '/../layouts/email_layout.php'; ?>
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
/**
|
||||
* File: verify_contact.php
|
||||
* Path: /resources/views/emails/verify_contact.php
|
||||
* Template Name: Verify Contact Email
|
||||
* Purpose: Email verification message for contact form submissions.
|
||||
* Triggered By: EmailService::sendVerificationEmail(..., 'verify_contact')
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
/** @var string $first_name */
|
||||
/** @var string $verification_link */
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verify Your Email</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; color: #333; background: #f9f9f9; padding: 40px; }
|
||||
.container { max-width: 640px; background: #fff; padding: 30px; border-radius: 6px; margin: auto; border: 1px solid #ddd; }
|
||||
h1 { color: #005baa; }
|
||||
.cta { display: inline-block; background-color: #005baa; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin-top: 20px; }
|
||||
.cta:hover { background-color: #004080; }
|
||||
.footer { margin-top: 30px; font-size: 0.85em; color: #777; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Hi <?= htmlspecialchars($first_name ?? 'there') ?>,</h1>
|
||||
|
||||
<p>Thanks for reaching out to Wizdom Networks. To ensure we received your inquiry and can respond to you promptly, please confirm your email address below:</p>
|
||||
|
||||
<p><a class="cta" href="<?= htmlspecialchars($verification_link) ?>">Verify My Email</a></p>
|
||||
|
||||
<p class="footer">This email was triggered by a contact form submission on wizdom.ca. If you didn’t make this request, you can safely ignore it.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
/**
|
||||
* File: verify_email.php
|
||||
* Version: 1.0
|
||||
* Path: /resources/views/emails/verify_email.php
|
||||
* Purpose: Content block for verification emails.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<p>Hello <?= htmlspecialchars($first_name ?? 'there') ?>,</p>
|
||||
|
||||
<p>Thank you for connecting with Wizdom Networks. Please confirm your email by clicking the link below:</p>
|
||||
|
||||
<p><a href="<?= htmlspecialchars($verification_link) ?>">Verify My Email</a></p>
|
||||
|
||||
<p>This link will expire in 48 hours.</p>
|
||||
|
||||
<p>If you didn’t request this, please ignore this email.</p>
|
||||
|
||||
<p>— The Wizdom Networks Team</p>
|
||||
<?php $body = ob_get_clean(); ?>
|
||||
|
||||
<?php include __DIR__ . '/../layouts/email_layout.php'; ?>
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/**
|
||||
* File: verify_newsletter.php
|
||||
* Path: /resources/views/emails/verify_newsletter.php
|
||||
* Template Name: Verify Newsletter
|
||||
* Purpose: Email template for newsletter opt-in verification (double opt-in).
|
||||
* Triggered By: EmailService::sendVerificationEmail(..., 'verify_newsletter')
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
/** @var string $first_name */
|
||||
/** @var string $verification_link */
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirm Your Subscription</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f9f9f9; color: #333; padding: 30px; }
|
||||
.container { background: #fff; border: 1px solid #ddd; padding: 25px; max-width: 640px; margin: auto; border-radius: 6px; }
|
||||
h1 { color: #005baa; }
|
||||
.cta { display: inline-block; background: #005baa; color: white; padding: 12px 24px; margin-top: 20px; text-decoration: none; border-radius: 4px; }
|
||||
.cta:hover { background: #004a91; }
|
||||
.footer { font-size: 0.85em; color: #777; margin-top: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to Wizdom<?= isset($first_name) ? ", $first_name" : '' ?>!</h1>
|
||||
|
||||
<p>You're almost done! Please confirm your subscription to the Wizdom Networks newsletter by verifying your email address.</p>
|
||||
|
||||
<p><a class="cta" href="<?= htmlspecialchars($verification_link) ?>">Confirm My Subscription</a></p>
|
||||
|
||||
<p class="footer">
|
||||
If you didn’t request to join our newsletter, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -37,9 +37,11 @@
|
|||
<script src="/assets/vendor/swiper/swiper-bundle.min.js"></script>
|
||||
<script src="/assets/vendor/waypoints/noframework.waypoints.js"></script>
|
||||
<script src="/assets/vendor/imagesloaded/imagesloaded.pkgd.min.js"></script>
|
||||
|
||||
<script src="/assets/js/contact-form.js"></script>
|
||||
|
||||
<!-- Conditional Form Script -->
|
||||
<!--
|
||||
<script>
|
||||
if (document.querySelector('.php-email-form')) {
|
||||
const script = document.createElement('script');
|
||||
|
|
@ -47,10 +49,9 @@
|
|||
document.body.appendChild(script);
|
||||
}
|
||||
</script>
|
||||
|
||||
-->
|
||||
<!-- Main JS File (must come last) -->
|
||||
<script src="/assets/js/main.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
/**
|
||||
* File: email_layout.php
|
||||
* Version: 1.0
|
||||
* Path: /resources/views/emails/layout/email_layout.php
|
||||
* Purpose: Base layout for all outgoing Wizdom Networks HTML emails.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= htmlspecialchars($subject ?? 'Wizdom Networks') ?></title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color: #f7f7f7; margin: 0; padding: 0; }
|
||||
.email-container { width: 100%; max-width: 600px; margin: 0 auto; background: #ffffff; border: 1px solid #ddd; }
|
||||
.email-header { background: #001e38; padding: 20px; text-align: center; }
|
||||
.email-header img { max-height: 50px; }
|
||||
.email-body { padding: 20px; color: #333333; font-size: 16px; line-height: 1.5; }
|
||||
.email-footer { background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #777; }
|
||||
a { color: #0056b3; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<img src="<?= $_ENV['APP_URL'] ?>/assets/img/wizdom-networks-logo-v2.png" alt="Wizdom Networks">
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<?= $body ?? '' ?>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
© <?= date('Y') ?> Wizdom Networks. All rights reserved.<br>
|
||||
<a href="<?= $_ENV['APP_URL'] ?>">www.wizdom.ca</a> | 416-USE-WISE
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div class="col-lg-4 col-md-6 footer-about">
|
||||
<a href="/" class="d-flex align-items-center">
|
||||
<span class="sitename">Wizdom Networks</span>
|
||||
<img src="/assets/img/wizdom-networks-logo-v2-no-slogan.webp" alt="Wizdom Networks Logo" class="img-fluid">
|
||||
</a>
|
||||
<div class="footer-contact pt-3">
|
||||
<p>Mississauga, ON, Canada</p>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
/**
|
||||
* File: verify_failed.php
|
||||
* Version: 1.1
|
||||
* Path: /resources/views/pages/verify_failed.php
|
||||
* Purpose: Displays failure message and allows resend of verification links.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
// File: verify_failed.php
|
||||
// Version: 1.1
|
||||
// Path: /resources/views/pages/verify_failed.php
|
||||
// Purpose: Displays failure message and allows resend of verification links.
|
||||
// Project: Wizdom Networks Website
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<section class="verify-section section pt-5">
|
||||
<div class="container text-center">
|
||||
<div class="icon mb-4">
|
||||
|
|
@ -21,6 +24,7 @@
|
|||
<img src="/assets/img/newsletter-thanks.webp" alt="Error" class="img-fluid rounded shadow" style="max-width: 400px;">
|
||||
</div>
|
||||
|
||||
<?php if (empty($redirect)): ?>
|
||||
<!-- Resend Verification Form -->
|
||||
<div class="mt-4">
|
||||
<h5>Resend Verification</h5>
|
||||
|
|
@ -30,20 +34,26 @@
|
|||
<input type="email" name="email" class="form-control" placeholder="Enter your email" required>
|
||||
</div>
|
||||
<div class="col-12 mb-2">
|
||||
<select name="type" class="form-select" required>
|
||||
<option value="">Select type...</option>
|
||||
<option value="newsletter">Newsletter</option>
|
||||
<option value="contact">Contact Form</option>
|
||||
</select>
|
||||
<input type="hidden" name="type" value="<?= htmlspecialchars($type ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">Resend Link</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mt-4">
|
||||
<a href="/" class="btn btn-primary">Start a New Submission</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="/" class="btn btn-outline-primary">Return to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
$html = ob_get_clean();
|
||||
View::render('layouts/arsha', ['content' => $html]);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
// File: verify_success.php
|
||||
// Version: 1.2
|
||||
// Purpose: Displays confirmation message after email verification and optional personalization update.
|
||||
|
||||
use WizdomNetworks\WizeWeb\Core\View;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<section class="verify-section section pt-5">
|
||||
<div class="container text-center">
|
||||
<div class="icon mb-4">
|
||||
|
|
@ -51,3 +56,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
$html = ob_get_clean();
|
||||
View::render('layouts/arsha', ['content' => $html]);
|
||||
?>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* File: contact.php
|
||||
* Version: 1.3
|
||||
* Version: 1.4
|
||||
* Path: /resources/views/partials/contact.php
|
||||
* Purpose: Renders the contact form with inline error feedback and optional newsletter signup.
|
||||
* Project: Wizdom Networks Website
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
<!-- Contact Form -->
|
||||
<div class="col-lg-7">
|
||||
<form action="/contact" method="post" class="php-email-form" data-aos="fade-up" data-aos-delay="200">
|
||||
<form action="/contact" method="post" class="php-email-form" onsubmit="return false;" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="row gy-4">
|
||||
|
||||
<!-- Inline error message -->
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-12 form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="subscribe_newsletter" id="subscribe-newsletter">
|
||||
<input class="form-check-input" type="checkbox" name="subscribe_newsletter" value="1" id="subscribe-newsletter">
|
||||
<label class="form-check-label" for="subscribe-newsletter">
|
||||
Sign me up for the Wizdom Networks newsletter
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
delete from verification_attempts;
|
||||
delete from subscribers;
|
||||
delete from contact_messages;
|
||||
delete from submission_logs;
|
||||
Loading…
Reference in New Issue