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:
essae 2025-05-23 14:23:08 -04:00
parent fc4e8ae851
commit e09f763db3
36 changed files with 1254 additions and 841 deletions

View File

@ -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;
}
}

View File

@ -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.']);
}
}
}

View File

@ -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']
]);
}
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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
]);
}
}
}

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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.'];
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>Well be in touch shortly. If its 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 '';
}
}
}

View File

@ -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 [];
}
}
}

View File

@ -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

View File

@ -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';

View File

@ -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');

View File

@ -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+. Weve received your message and will get back to you shortly. Heres 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">
&copy; 2025 HelpDesk+. All rights reserved.
</div>
</div>
</body>
</html>

View File

@ -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'; ?>

View File

@ -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>Heres 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! Well 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>

View File

@ -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>

View File

@ -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'; ?>

View File

@ -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'; ?>

View File

@ -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>

View File

@ -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>Well 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'; ?>

View File

@ -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 didnt make this request, you can safely ignore it.</p>
</div>
</body>
</html>

View File

@ -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 didnt 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'; ?>

View File

@ -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 didnt request to join our newsletter, you can safely ignore this email.
</p>
</div>
</body>
</html>

View File

@ -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>

View File

@ -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">
&copy; <?= date('Y') ?> Wizdom Networks. All rights reserved.<br>
<a href="<?= $_ENV['APP_URL'] ?>">www.wizdom.ca</a> | 416-USE-WISE
</div>
</div>
</body>
</html>

View File

@ -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>

View File

@ -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]);
?>

View File

@ -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]);
?>

View File

@ -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>

View File

@ -0,0 +1,4 @@
delete from verification_attempts;
delete from subscribers;
delete from contact_messages;
delete from submission_logs;