diff --git a/app/Controllers/ContactController.php b/app/Controllers/ContactController.php index e31a2ad..e6c4ddd 100644 --- a/app/Controllers/ContactController.php +++ b/app/Controllers/ContactController.php @@ -1,9 +1,11 @@ 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; + } } diff --git a/app/Controllers/ResendVerficationController.php b/app/Controllers/ResendVerficationController.php deleted file mode 100644 index 1742120..0000000 --- a/app/Controllers/ResendVerficationController.php +++ /dev/null @@ -1,104 +0,0 @@ - '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.']); - } - } -} \ No newline at end of file diff --git a/app/Controllers/ResendVerificationController.php b/app/Controllers/ResendVerificationController.php new file mode 100644 index 0000000..4a236ad --- /dev/null +++ b/app/Controllers/ResendVerificationController.php @@ -0,0 +1,60 @@ +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'] + ]); + } + } +} diff --git a/app/Controllers/SubscriberController.php b/app/Controllers/SubscriberController.php index 6f8affb..d9fa604 100644 --- a/app/Controllers/SubscriberController.php +++ b/app/Controllers/SubscriberController.php @@ -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 { diff --git a/app/Controllers/UnsubscribeController.php b/app/Controllers/UnsubscribeController.php index 8d8d161..59c1bb7 100644 --- a/app/Controllers/UnsubscribeController.php +++ b/app/Controllers/UnsubscribeController.php @@ -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 { diff --git a/app/Controllers/VerificationController.php b/app/Controllers/VerificationController.php index f372abc..9bf13d0 100644 --- a/app/Controllers/VerificationController.php +++ b/app/Controllers/VerificationController.php @@ -1,71 +1,177 @@ '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 + ]); } } + +} diff --git a/app/Models/ContactModel.php b/app/Models/ContactModel.php index 9d7218a..b8edc68 100644 --- a/app/Models/ContactModel.php +++ b/app/Models/ContactModel.php @@ -1,9 +1,9 @@ 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()); diff --git a/app/Services/EmailService.php b/app/Services/EmailService.php new file mode 100644 index 0000000..4c1e8bb --- /dev/null +++ b/app/Services/EmailService.php @@ -0,0 +1,165 @@ +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); + } +} diff --git a/app/Services/NewsletterService.php b/app/Services/NewsletterService.php index 4446bf7..952ad24 100644 --- a/app/Services/NewsletterService.php +++ b/app/Services/NewsletterService.php @@ -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(); diff --git a/app/Services/ResendVerificationService.php b/app/Services/ResendVerificationService.php new file mode 100644 index 0000000..ccafbb6 --- /dev/null +++ b/app/Services/ResendVerificationService.php @@ -0,0 +1,110 @@ +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.']; + } + } +} diff --git a/app/Services/VerificationService.php b/app/Services/VerificationService.php new file mode 100644 index 0000000..e3c803d --- /dev/null +++ b/app/Services/VerificationService.php @@ -0,0 +1,104 @@ +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; + } + } +} diff --git a/app/Utilities/EmailHelper.php b/app/Utilities/EmailHelper.php index 7ec3b39..62f4a3b 100644 --- a/app/Utilities/EmailHelper.php +++ b/app/Utilities/EmailHelper.php @@ -1,9 +1,9 @@ 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 " - Name: {$data['first_name']} {$data['last_name']}
- Email: {$data['email']}
- Phone: {$data['phone']}
- Subject: {$data['subject']}
- Message:
-
{$data['message']}
- "; - } - - 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 = " - Context: {$context}
- Error Message:
-
{$errorMessage}
- "; - - if (!empty($data)) { - $body .= "
Associated Data:
"; - } - - return $body; - } - - private static function buildSalesHtmlBody(array $data): string - { - $submittedAt = date('Y-m-d H:i:s'); - return " -

New contact submission received on {$submittedAt}.

- -
-

IP: {$data['ip_address']}
User-Agent: {$data['user_agent']}

- "; - } - - private static function buildConfirmationHtmlBody(array $data): string - { - $submittedAt = date('Y-m-d H:i:s'); - return " -

Hi {$data['first_name']},

-

Thank you for contacting Wizdom Networks. This message confirms that we received your inquiry on {$submittedAt}.

- -

We’ll be in touch shortly. If it’s urgent, call us at 416-USE-WISE.

-

IP: {$data['ip_address']}
User-Agent: {$data['user_agent']}

- "; - } - - 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 ''; } } } diff --git a/app/Utilities/EmailUtility.php b/app/Utilities/EmailUtility.php deleted file mode 100644 index cc42b91..0000000 --- a/app/Utilities/EmailUtility.php +++ /dev/null @@ -1,393 +0,0 @@ - $_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 []; - } -} -} diff --git a/app/Utilities/SubmissionCheck.php b/app/Utilities/SubmissionCheck.php index 9e03213..6676f95 100644 --- a/app/Utilities/SubmissionCheck.php +++ b/app/Utilities/SubmissionCheck.php @@ -1,7 +1,7 @@ = 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); diff --git a/public/assets/img/wizdom-networks-logo-v2-no-slogan-blue-1119x303.png b/public/assets/img/wizdom-networks-logo-v2-no-slogan-blue-1119x303.png new file mode 100644 index 0000000..dd6fe79 Binary files /dev/null and b/public/assets/img/wizdom-networks-logo-v2-no-slogan-blue-1119x303.png differ diff --git a/public/assets/img/wizdom-networks-logo-v2.png b/public/assets/img/wizdom-networks-logo-v2.png new file mode 100644 index 0000000..fef9f97 Binary files /dev/null and b/public/assets/img/wizdom-networks-logo-v2.png differ diff --git a/public/assets/js/contact-form.js b/public/assets/js/contact-form.js index fffed69..7b15465 100644 --- a/public/assets/js/contact-form.js +++ b/public/assets/js/contact-form.js @@ -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'; diff --git a/public/index.php b/public/index.php index ab08efe..86acea3 100644 --- a/public/index.php +++ b/public/index.php @@ -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'); diff --git a/resources/templates/emails/contact_confirmation_email.html b/resources/templates/emails/contact_confirmation_email.html deleted file mode 100644 index dcf074a..0000000 --- a/resources/templates/emails/contact_confirmation_email.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - Email from HelpDesk+ - - - -
-
-

HelpDesk+

-
-
-

Dear {{name}},

- -

Thank you for reaching out to HelpDesk+. We’ve received your message and will get back to you shortly. Here’s what you submitted:

- -
-

Message: {{message}}

-
- -

If you have any further questions or concerns, feel free to reply to this email.

- -

Best regards,
The HelpDesk+ Team

-
- -
- - diff --git a/resources/views/emails/admin_contact_alert.php b/resources/views/emails/admin_contact_alert.php new file mode 100644 index 0000000..3c62596 --- /dev/null +++ b/resources/views/emails/admin_contact_alert.php @@ -0,0 +1,22 @@ + + + +

New Contact Form Submission Received

+ +

Name:
+Email:
+IP Address:
+User Agent:

+ +

Message:

+ + + + diff --git a/resources/views/emails/contact_and_newsletter.php b/resources/views/emails/contact_and_newsletter.php new file mode 100644 index 0000000..29d1e55 --- /dev/null +++ b/resources/views/emails/contact_and_newsletter.php @@ -0,0 +1,61 @@ + + + + + + + + Thanks for Contacting Us & Welcome + + + +
+

Thanks for reaching out!

+ +

We've received your message and appreciate you taking the time to contact Wizdom Networks. A member of our team will follow up shortly.

+ +

Here’s what you submitted:

+ + +

You also signed up for our newsletter — welcome aboard! We’ll occasionally send you updates, tips, and insights straight from our team.

+ +

What to expect:

+ + + +
+ + diff --git a/resources/views/emails/newsletter_welcome.php b/resources/views/emails/newsletter_welcome.php new file mode 100644 index 0000000..3296d0f --- /dev/null +++ b/resources/views/emails/newsletter_welcome.php @@ -0,0 +1,71 @@ + + + + + + + Welcome to Wizdom Networks + + + +
+

Welcome to Wizdom Networks!

+ +

Thanks for subscribing to the Wizdom Networks newsletter. We're glad to have you with us.

+ +

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.

+ +

If you ever want to update your preferences or tell us more about yourself, just click below.

+ + Update Preferences + + +
+ + diff --git a/resources/views/emails/resend_verification.php b/resources/views/emails/resend_verification.php new file mode 100644 index 0000000..ad7f8c6 --- /dev/null +++ b/resources/views/emails/resend_verification.php @@ -0,0 +1,24 @@ + + + + +

Hello ,

+ +

You recently requested to re-send your email verification link. Please click below to verify your email address:

+ +

Verify My Email

+ +

If you didn't request this, no further action is needed.

+ +

— The Wizdom Networks Team

+ + + + diff --git a/resources/views/emails/sales_lead_alert.php b/resources/views/emails/sales_lead_alert.php new file mode 100644 index 0000000..76be271 --- /dev/null +++ b/resources/views/emails/sales_lead_alert.php @@ -0,0 +1,21 @@ + + + +

New Sales Lead Submitted

+ +

Name:
+Email:
+Phone:

+ +

Message:

+ + + + \ No newline at end of file diff --git a/resources/views/emails/system_alert.php b/resources/views/emails/system_alert.php new file mode 100644 index 0000000..2ea9484 --- /dev/null +++ b/resources/views/emails/system_alert.php @@ -0,0 +1,57 @@ + + + + + + + + System Alert + + + +
+

🚨 System Alert:

+ +

Error Message:

+
+ + +

Contextual Data:

+ + + + +
+ + diff --git a/resources/views/emails/verified_confirmation.php b/resources/views/emails/verified_confirmation.php new file mode 100644 index 0000000..2a8b2dc --- /dev/null +++ b/resources/views/emails/verified_confirmation.php @@ -0,0 +1,28 @@ + + + +

Hello ,

+ +

Your email has been successfully verified. Thank you for connecting with Wizdom Networks.

+ +

Here's a summary of your submission:

+ + +

We’ll be in touch soon if your message requires a response. In the meantime, feel free to reach out at 416-USE-WISE if you need immediate assistance.

+ +

— The Wizdom Networks Team

+ + + + diff --git a/resources/views/emails/verify_contact.php b/resources/views/emails/verify_contact.php new file mode 100644 index 0000000..38a5cd5 --- /dev/null +++ b/resources/views/emails/verify_contact.php @@ -0,0 +1,41 @@ + + + + + + + + Verify Your Email + + + +
+

Hi ,

+ +

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:

+ +

Verify My Email

+ + +
+ + \ No newline at end of file diff --git a/resources/views/emails/verify_email.php b/resources/views/emails/verify_email.php new file mode 100644 index 0000000..36c6a14 --- /dev/null +++ b/resources/views/emails/verify_email.php @@ -0,0 +1,26 @@ + + + +

Hello ,

+ +

Thank you for connecting with Wizdom Networks. Please confirm your email by clicking the link below:

+ +

Verify My Email

+ +

This link will expire in 48 hours.

+ +

If you didn’t request this, please ignore this email.

+ +

— The Wizdom Networks Team

+ + + + diff --git a/resources/views/emails/verify_newsletter.php b/resources/views/emails/verify_newsletter.php new file mode 100644 index 0000000..c83ef5b --- /dev/null +++ b/resources/views/emails/verify_newsletter.php @@ -0,0 +1,43 @@ + + + + + + + + Confirm Your Subscription + + + +
+

Welcome to Wizdom!

+ +

You're almost done! Please confirm your subscription to the Wizdom Networks newsletter by verifying your email address.

+ +

Confirm My Subscription

+ + +
+ + diff --git a/resources/views/layouts/arsha.php b/resources/views/layouts/arsha.php index eb269c7..7c14f82 100644 --- a/resources/views/layouts/arsha.php +++ b/resources/views/layouts/arsha.php @@ -37,9 +37,11 @@ + + - diff --git a/resources/views/layouts/email_layout.php b/resources/views/layouts/email_layout.php new file mode 100644 index 0000000..4c19afc --- /dev/null +++ b/resources/views/layouts/email_layout.php @@ -0,0 +1,40 @@ + + + + + + + <?= htmlspecialchars($subject ?? 'Wizdom Networks') ?> + + + +
+
+ Wizdom Networks +
+
+ +
+ +
+ + diff --git a/resources/views/layouts/footer.php b/resources/views/layouts/footer.php index 978b7cb..26618a2 100644 --- a/resources/views/layouts/footer.php +++ b/resources/views/layouts/footer.php @@ -28,7 +28,7 @@ + $html]); +?> diff --git a/resources/views/pages/verify_success.php b/resources/views/pages/verify_success.php index 6a29d80..b7fbdd6 100644 --- a/resources/views/pages/verify_success.php +++ b/resources/views/pages/verify_success.php @@ -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(); ?> +
@@ -51,3 +56,7 @@
+ $html]); +?> \ No newline at end of file diff --git a/resources/views/partials/contact.php b/resources/views/partials/contact.php index 02b6099..cd75cc5 100644 --- a/resources/views/partials/contact.php +++ b/resources/views/partials/contact.php @@ -1,7 +1,7 @@
-
+
@@ -102,7 +102,7 @@
- + diff --git a/scripts/empty_database.sql b/scripts/empty_database.sql new file mode 100644 index 0000000..9b1e63e --- /dev/null +++ b/scripts/empty_database.sql @@ -0,0 +1,4 @@ +delete from verification_attempts; +delete from subscribers; +delete from contact_messages; +delete from submission_logs; \ No newline at end of file