Compare commits

...

16 Commits

Author SHA1 Message Date
essae 70105e095c feat: Soft-launch beta go-live version with hidden sections and pending tweaks
- Marked current state as public soft launch
- Temporarily hidden incomplete sections
- Confirmed routing, hero/slider logic loads without error
- Prepared for incremental SEO, UI, and functionality improvements
2025-06-02 16:42:38 -04:00
essae 5ec5195d89 feat(email): implement secure unsubscribe link generator and email helper integration 2025-05-24 20:06:33 -04:00
essae cf146973f2 feat(unsubscribe): secure token-based unsubscribe flow with flexible services
- Created TokenService for generic HMAC token generation and validation
- Created UnsubscribeTokenService to wrap TokenService with email+timestamp logic and TTL
- Updated UnsubscribeController to require valid signed tokens for GET /unsubscribe
- Token is generated using email + ts + shared secret, validated against TTL (default 24h)
- Confirm view now inaccessible unless accessed via system-generated link
- Deprecated static helper in favor of service architecture
2025-05-24 17:21:52 -04:00
essae e09f763db3 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
2025-05-23 14:23:08 -04:00
essae fc4e8ae851 feat: Implement email verification and refactor form logic (partial)
- Added ContactService.php and NewsletterService.php for handling verification emails
- Introduced POST-based verification and session-driven success rendering
- Replaced redirect-based confirmation with inline JS and conditional rendering
- Reorganized routes in index.php for controller and closure-based actions
- Minor JS improvements to prevent double submission (contact-form.js)
- Honeypot field temporarily missing despite prior implementation and HoneypotHelper
- SubmissionCheck needs fix for SQL parameter mismatch
- EmailHelper missing getMailer() (referenced in services)
- General structure drift noted — inventory and cleanup pending
2025-05-20 20:52:46 -04:00
essae a1c25d4885 Update landing.php to v1.2.2 – final layout and copy for one-pager 2025-05-16 11:04:35 -04:00
essae 1d96ccd3c1 Add Bootstrap dismissible alert for contact form success and error messages 2025-05-15 00:37:22 -04:00
essae 4e35d36485 Fix session handling for contact form success/error alerts and relocate SessionHelper::start in index.php 2025-05-14 23:54:31 -04:00
essae 7a0594d4f5 more contact form updates 2025-05-13 13:52:18 -04:00
essae b48a5f8e0c minor updates 2025-05-13 01:20:16 -04:00
essae 761c41d3bb Fix: Updated ContactController to use Response::error(), injected DB connection into ContactModel, added GET route for contact page in index.php 2025-05-13 00:36:15 -04:00
essae e4ff1f0a59 Integrate EmailUtility, SubmissionCheck, and Response into ContactController; finalize contact form logic and template handling 2025-05-12 15:06:39 -04:00
essae 90b7b0b785 Contact form initial updates 2025-05-10 16:55:50 -04:00
essae c309fa1eee 🎨 Integrated Arsha template as v1 landing page 2025-05-10 14:02:02 -04:00
essae a258493698 namespace typo fixes 2025-05-03 16:35:04 -04:00
essae 0dea9b9c7c Add LandingController and Arsha layout integration 2025-05-03 16:22:24 -04:00
634 changed files with 95490 additions and 563 deletions

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class AboutController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class ClientsController
{

View File

@ -1,38 +1,186 @@
<?php
/**
* File: ContactController.php
* Version: 2.16
* Path: /app/Controllers/ContactController.php
* 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
*/
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\Validator;
use WizdomNetworks\WizeWeb\Utilities\Sanitizer;
use WizdomNetworks\WizeWeb\Utilities\Database;
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 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
{
Logger::debug("ContactController::index() - Executing contact page rendering.");
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");
try {
// Prepare data for the contact page
$data = [
'title' => 'Contact Us - Wizdom Networks',
'heroConfig' => [
'title' => 'Get in Touch',
'description' => 'Reach out to our team for inquiries and support.',
'image' => '/assets/images/contact-hero.jpg',
'cta' => ['text' => 'Send a Message', 'link' => '/contact'],
'style' => 'default',
'position' => 'top'
],
'content' => "<h1>Contact Us</h1>
<p>We're here to help. Send us a message and we'll get back to you as soon as possible.</p>"
$formData = [
'first_name' => Sanitizer::sanitizeString($_POST['first_name'] ?? ''),
'last_name' => Sanitizer::sanitizeString($_POST['last_name'] ?? ''),
'email' => Sanitizer::sanitizeString($_POST['email'] ?? ''),
'phone' => Sanitizer::sanitizeString($_POST['phone'] ?? ''),
'subject' => Sanitizer::sanitizeString($_POST['subject'] ?? ''),
'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
];
Logger::debug("ContactController::index() - Data prepared successfully.");
View::render('pages/contact', $data);
Logger::info("ContactController::index() - Contact page rendered successfully.");
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']) ||
empty($formData['email']) ||
empty($formData['phone']) ||
empty($formData['subject']) ||
empty($formData['message']) ||
!Validator::isEmail($formData['email'])
) {
Logger::info("Validation failed for contact form submission");
$_SESSION['contact_error'] = 'Validation error. Please try again.';
SessionHelper::writeClose();
$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']}");
$this->emailService->alertAdmins('Blocked Submission Detected', $evaluation['reason'], $formData);
SessionHelper::writeClose();
$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)");
$logStmt->execute([
':email' => $formData['email'],
':phone' => $formData['phone'],
':ip' => $formData['ip_address'],
':ua' => $formData['user_agent'],
':saved' => 0,
':reason' => $evaluation['reason'],
]);
$logId = $db->lastInsertId();
} catch (\Throwable $e) {
Logger::error("Failed to insert into submission_logs: " . $e->getMessage());
}
// Save form content
$contactModel = new ContactModel($db);
$saveSuccess = $contactModel->saveContactForm($formData);
$contactId = $db->lastInsertId();
// Assign verification code
if ($saveSuccess) {
$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]);
}
SessionHelper::writeClose();
$this->respondOrRedirect(true, 'Your message was submitted. Please check your email to verify.');
} catch (\Throwable $e) {
Logger::error("ContactController::index() - Error rendering contact page: " . $e->getMessage());
ErrorHandler::exception($e);
Logger::error("Fatal error in ContactController::submit: " . $e->getMessage());
$this->emailService->alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
$_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
SessionHelper::writeClose();
$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

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class EmergencySupportController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class HelpDeskController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class HomeController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class ITConsultingController
{

View File

@ -0,0 +1,28 @@
<?php
// File: app/Controllers/LandingController.php
// Version: v1.1
// Purpose: Handles landing page rendering for Arsha one-pager
// Project: Wizdom Networks Website
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\SessionHelper;
class LandingController
{
public function index(): void
{
SessionHelper::start(); // ✅ Start session before rendering
Logger::info("Session status: " . session_status());
Logger::info("📥 Landing page session ID: " . session_id());
Logger::info("🟡 Landing page session before render: " . json_encode($_SESSION));
$data = [
'pageTitle' => 'Wizdom Networks | One-Pager'
];
View::render('pages/landing', $data, 'arsha');
}
}

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class ManagedServicesController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class OnlineBrandManagementController
{

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class ProjectManagementController
{

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

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class ServicesController
{

View File

@ -0,0 +1,68 @@
<?php
/**
* File: SubscriberController.php
* Version: 1.1
* Path: /app/Controllers/SubscriberController.php
* Purpose: Handles subscriber updates including optional name personalization.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Database;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class SubscriberController
{
/**
* POST /subscriber/update
* Allows a verified subscriber to add their name.
*/
public function update(): void
{
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo "Method Not Allowed";
return;
}
$email = trim($_POST['email'] ?? '');
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
if (empty($email)) {
Logger::error("Subscriber update failed: email missing.");
View::render('pages/verify_failed', ['reason' => 'Missing email address.']);
return;
}
$db = Database::getConnection();
$stmt = $db->prepare("SELECT id FROM subscribers WHERE email = ?");
$stmt->execute([$email]);
$subscriber = $stmt->fetch();
if (!$subscriber) {
Logger::error("Subscriber update failed: not found [$email].");
View::render('pages/verify_failed', ['reason' => 'Subscriber not found.']);
return;
}
$stmt = $db->prepare("UPDATE subscribers SET first_name = ?, last_name = ? WHERE id = ?");
$stmt->execute([$firstName, $lastName, $subscriber['id']]);
Logger::info("Subscriber updated: $email");
$_SESSION['update_success'] = true;
$_SESSION['update_type'] = 'newsletter';
header("Location: /verify-success");
exit;
} catch (\Throwable $e) {
Logger::error("Subscriber update error for $email: " . $e->getMessage());
ErrorHandler::exception($e);
View::render('pages/verify_failed', ['reason' => 'An error occurred while updating your info.']);
}
}
}

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class TestimonialsController
{

View File

@ -0,0 +1,96 @@
<?php
/**
* File: UnsubscribeController.php
* Version: 1.0
* Path: app/Controllers/
* Purpose: Handles newsletter unsubscribe confirmation and processing.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Database;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class UnsubscribeController
{
/**
* GET /unsubscribe
* Show confirmation form for unsubscribing.
*/
public function confirm(): void
{
try {
$email = trim($_GET['email'] ?? '');
if (empty($email)) {
Logger::error("Unsubscribe access without email.");
View::render('pages/unsubscribe_failed', ['reason' => 'No email provided.']);
return;
}
$db = Database::getConnection();
$stmt = $db->prepare("SELECT is_verified, unsubscribed_at FROM subscribers WHERE email = ?");
$stmt->execute([$email]);
$subscriber = $stmt->fetch();
if (!$subscriber) {
Logger::error("Unsubscribe: Subscriber not found [$email]");
View::render('pages/unsubscribe_failed', ['reason' => 'Subscriber not found.']);
return;
}
if ($subscriber['unsubscribed_at']) {
View::render('pages/unsubscribe_success', ['email' => $email, 'alreadyUnsubscribed' => true]);
return;
}
View::render('pages/unsubscribe_confirm', ['email' => $email]);
} catch (\Throwable $e) {
Logger::error("Unsubscribe view error: " . $e->getMessage());
ErrorHandler::exception($e);
View::render('pages/unsubscribe_failed', ['reason' => 'An unexpected error occurred.']);
}
}
/**
* POST /unsubscribe
* Perform the actual unsubscribe action.
*/
public function process(): void
{
try {
$email = trim($_POST['email'] ?? '');
$reason = trim($_POST['unsubscribe_reason'] ?? '');
if (empty($email)) {
Logger::error("Unsubscribe form submitted without email.");
View::render('pages/unsubscribe_failed', ['reason' => 'No email address was provided.']);
return;
}
$db = Database::getConnection();
$stmt = $db->prepare("SELECT id FROM subscribers WHERE email = ?");
$stmt->execute([$email]);
$subscriber = $stmt->fetch();
if (!$subscriber) {
Logger::error("Unsubscribe: Subscriber not found during processing [$email]");
View::render('pages/unsubscribe_failed', ['reason' => 'Subscriber not found.']);
return;
}
$stmt = $db->prepare("UPDATE subscribers SET unsubscribed_at = NOW(), unsubscribe_reason = ? WHERE id = ?");
$stmt->execute([$reason, $subscriber['id']]);
Logger::info("Subscriber unsubscribed: $email");
View::render('pages/unsubscribe_success', ['email' => $email]);
} catch (\Throwable $e) {
Logger::error("Unsubscribe processing error for $email: " . $e->getMessage());
ErrorHandler::exception($e);
View::render('pages/unsubscribe_failed', ['reason' => 'An error occurred while processing your unsubscribe.']);
}
}
}

View File

@ -0,0 +1,177 @@
<?php
/**
* File: VerificationController.php
* Version: 1.11
* 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\Utilities\Database;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Services\EmailService;
class VerificationController
{
private EmailService $emailService;
public function __construct()
{
$this->emailService = new EmailService();
}
/**
* 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;
}
$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();
if ($subscriber) {
$table = 'contact_messages';
$type = 'contact';
}
}
// 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;
}
// 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;
}
// 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 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);
}
$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

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Core;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Base Controller

View File

@ -1,70 +1,138 @@
<?php
/**
* ============================================
* File: Router.php
* Path: /app/Core/
* Purpose: Core router handling HTTP methodspecific route dispatching with dynamic path and closure support.
* Version: 1.4
* Author: Wizdom Networks
* ============================================
*/
namespace WizdomNetworks\WizeWeb\Core;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
use WizdomNetworks\WizeWeb\Core\View;
/**
* Router Class
*
* Handles application routing by mapping URL paths to controller methods.
* Ensures all requests are routed through controllers and logs dispatch details.
*/
class Router
{
/**
* Array of registered routes indexed by HTTP method and path.
* @var array
*/
private array $routes = [];
/**
* Registers a new route.
*
* @param string $path The URL path.
* @param string $controller The fully qualified controller class name.
* @param string $method The method within the controller.
* Registers a controller-based route with optional path parameters.
*
* @param string $path The route path (e.g. "/contact" or "/verify/{code}").
* @param string $controller Fully qualified controller class.
* @param string $method Method in controller to invoke.
* @param string $httpMethod HTTP method (GET, POST, etc.). Defaults to GET.
*/
public function add(string $path, string $controller, string $method): void
public function add(string $path, string $controller, string $method, string $httpMethod = 'GET'): void
{
Logger::debug("Registering route: $path -> $controller::$method");
$this->routes[trim($path, '/')] = [$controller, $method];
$normalizedPath = trim($path, '/');
$routeKey = strtoupper($httpMethod) . ':' . $normalizedPath;
// Transform path into regex pattern and extract parameter names
$paramKeys = [];
$regexPath = preg_replace_callback('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', function ($matches) use (&$paramKeys) {
$paramKeys[] = $matches[1];
return '([^\/]+)';
}, $normalizedPath);
$this->routes[$routeKey] = [
'controller' => $controller,
'method' => $method,
'pattern' => "#^" . $regexPath . "$#",
'params' => $paramKeys
];
Logger::debug("Registering route: [$httpMethod] $path -> $controller::$method");
}
/**
* Dispatches the request to the appropriate controller and method.
*
* @param string $path The requested URL path.
* Registers a closure-based route.
*
* @param string $path The route path.
* @param \Closure $callback Anonymous function to handle the route.
* @param string $httpMethod HTTP method (GET, POST, etc.). Defaults to GET.
*/
public function addClosure(string $path, \Closure $callback, string $httpMethod = 'GET'): void
{
$routeKey = strtoupper($httpMethod) . ':' . trim($path, '/');
Logger::debug("Registering closure route: [$httpMethod] $path");
$this->routes[$routeKey] = $callback;
}
/**
* Dispatches the current request to the matching route or fallback to 404.
*
* @param string $path The requested path, usually from index.php.
*/
public function dispatch($path)
{
$path = trim($path, '/');
Logger::debug("Dispatching path: $path");
$httpMethod = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$cleanPath = trim($path, '/');
$routeKeyBase = $httpMethod . ':';
if (isset($this->routes[$path])) {
[$controllerName, $method] = $this->routes[$path];
Logger::debug("Loading controller: $controllerName::$method");
foreach ($this->routes as $key => $route) {
if (strpos($key, $routeKeyBase) !== 0) {
continue;
}
try {
if (class_exists($controllerName)) {
// Handle closure route directly
if ($route instanceof \Closure && $key === $routeKeyBase . $cleanPath) {
Logger::info("Executing closure route: [$httpMethod] $cleanPath");
$route();
return;
}
// Only continue if route is an array
if (!is_array($route)) {
continue;
}
$routePattern = $route['pattern'] ?? null;
// Match dynamic route patterns and extract parameters
if ($routePattern && preg_match($routePattern, $cleanPath, $matches)) {
array_shift($matches); // Remove full match
$params = array_combine($route['params'], $matches) ?: [];
$controllerName = $route['controller'];
$method = $route['method'];
try {
if (!class_exists($controllerName)) {
throw new \Exception("Controller not found: $controllerName");
}
$controller = new $controllerName();
if (method_exists($controller, $method)) {
Logger::info("Successfully dispatched: $controllerName::$method");
$controller->$method();
} else {
Logger::error("Method not found: $controllerName::$method");
if (!method_exists($controller, $method)) {
throw new \Exception("Method $method not found in $controllerName");
}
} else {
Logger::error("Controller not found: $controllerName");
throw new \Exception("Controller $controllerName not found.");
Logger::info("Executing controller: $controllerName::$method with params: " . json_encode($params));
call_user_func_array([$controller, $method], $params);
return;
} catch (\Throwable $e) {
echo "<pre>";
echo "Exception: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . "\n";
echo "Line: " . $e->getLine() . "\n";
echo "Trace:\n" . $e->getTraceAsString();
echo "</pre>";
exit;
}
} catch (\Throwable $e) {
ErrorHandler::exception($e);
Logger::error("Router dispatch error: " . $e->getMessage());
echo "500 Internal Server Error";
}
} else {
Logger::error("Route not found: $path");
echo "404 Not Found";
}
// If no route matched, render 404 page
Logger::error("Route not found: [$httpMethod] $path");
http_response_code(404);
View::render('pages/404');
}
}

View File

@ -1,9 +1,19 @@
<?php
// ============================================
// File: View.php
// Version: 1.2
// Path: app/Core/View.php
// Purpose: Handles dynamic view rendering with optional layout wrapping
// Project: Wizdom Networks Website
// Usage: View::render('pages/landing', $data, 'arsha')
// ============================================
namespace WizdomNetworks\WizeWeb\Core;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* View Renderer
@ -13,34 +23,88 @@ use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
class View
{
/**
* Renders a view file and passes data to it.
* Renders a view file and optionally wraps it in a layout.
*
* @param string $view The name of the view file (relative to /resources/views/).
* @param array $data Associative array of variables to pass to the view.
* @throws \Exception If the view file is not found.
* @param string|null $layout The layout to use (relative to /resources/views/layouts/). Default is null (no layout).
* @throws \Exception If the view or layout file is not found.
*/
public static function render(string $view, array $data = []): void
public static function render(string $view, array $data = [], ?string $layout = null): void
{
Logger::debug("Rendering view: $view");
if (!class_exists('View')) {
class_alias(self::class, 'View');
}
// Extract data to make variables available in the view
extract($data);
// Build the full path to the view file
Logger::debug("[DEBUG] Attempting to load view: " . $view . " | Expected path: " . __DIR__ . "/../../resources/views/" . str_replace('.', '/', $view) . ".php");
$viewPath = realpath(__DIR__ . "/../../resources/views/" . str_replace('.', '/', $view) . ".php");
// Debugging: Log resolved path
Logger::debug("Resolved view path: $viewPath");
// Validate and include the view file
if ($viewPath && file_exists($viewPath)) {
include $viewPath;
Logger::debug("Successfully rendered view: $view");
} else {
// If using layout, resolve layout path
if ($layout) {
$layoutPath = realpath(__DIR__ . "/../../resources/views/layouts/" . $layout . ".php");
Logger::debug("Resolved layout path: $layoutPath");
if (!$layoutPath || !file_exists($layoutPath)) {
Logger::error("Layout file not found: $layout");
throw new \Exception("Layout file not found: $layout");
}
}
if (!$viewPath || !file_exists($viewPath)) {
Logger::error("View file not found: $view | Resolved path: $viewPath");
throw new \Exception("View file not found: $view");
}
// If using a layout, buffer content then inject into layout
if ($layout) {
ob_start();
include $viewPath;
$content = ob_get_clean();
include $layoutPath;
Logger::debug("Successfully rendered view: $view into layout: $layout");
} else {
include $viewPath;
Logger::debug("Successfully rendered view: $view (no layout)");
}
}
/**
* Renders a partial view without applying a layout.
*
* Use this for modular components like hero, services, faq, etc.
* Partial views must reside in: /resources/views/partials/
*
* @param string $partial The name of the partial (e.g., 'hero', 'faq').
* You may use dot notation for subdirectories (e.g., 'admin.nav').
* @param array $data Optional associative array of data to be extracted into the view.
*
* @throws \Exception if the partial file does not exist.
*
* @return void
*/
public static function renderPartial(string $partial, array $data = []): void
{
Logger::debug("Rendering partial: $partial");
// Convert dot notation to path and resolve to full filesystem path
$partialPath = realpath(__DIR__ . "/../../resources/views/partials/" . str_replace('.', '/', $partial) . ".php");
Logger::debug("Resolved partial path: $partialPath");
if (!$partialPath || !file_exists($partialPath)) {
Logger::error("Partial view not found: $partial | Resolved path: $partialPath");
throw new \Exception("Partial view not found: $partial");
}
// Extract data and include partial
extract($data);
include $partialPath;
Logger::debug("Successfully rendered partial: $partial");
}
}

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Models;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Client Model

View File

@ -1,101 +1,131 @@
<?php
/**
* File: ContactModel.php
* Version: 2.2
* Path: /app/Models/ContactModel.php
* Purpose: Manages saving and retrieving contact records from both legacy and full form submissions, including newsletter opt-in tracking.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Models;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use PDO;
use Exception;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Contact Model
*
* Handles database operations related to contacts.
*/
class ContactModel
{
private $db;
private PDO $db;
public function __construct($db)
/**
* ContactModel constructor.
*
* @param PDO $db Database connection
*/
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Retrieve a contact by ID.
*
* Legacy method to insert simplified contact into `contacts` table.
*
* @param array $contactData ['name' => string, 'email' => string, 'message' => string]
* @return bool
*/
public function addContact(array $contactData): bool
{
try {
$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);
$stmt->bindParam(':email', $contactData['email']);
$stmt->bindParam(':message', $contactData['message']);
return $stmt->execute();
} catch (Exception $e) {
Logger::error("ContactModel::addContact failed: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}
}
/**
* 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
*/
public function saveContactForm(array $formData): bool
{
try {
$stmt = $this->db->prepare("INSERT INTO contact_messages (
first_name, last_name, email, phone, subject, message,
ip_address, user_agent, pending_newsletter_opt_in
) VALUES (
:first_name, :last_name, :email, :phone, :subject, :message,
:ip_address, :user_agent, :pending_newsletter_opt_in
)");
$stmt->bindParam(':first_name', $formData['first_name']);
$stmt->bindParam(':last_name', $formData['last_name']);
$stmt->bindParam(':email', $formData['email']);
$stmt->bindParam(':phone', $formData['phone']);
$stmt->bindParam(':subject', $formData['subject']);
$stmt->bindParam(':message', $formData['message']);
$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());
ErrorHandler::exception($e);
return false;
}
}
/**
* Retrieves a contact record by ID from `contact_messages`.
*
* @param int $id
* @return array|null
*/
public function getContactById(int $id): ?array
{
try {
Logger::info("[DEBUG] Fetching contact with ID: $id");
$stmt = $this->db->prepare("SELECT * FROM contacts WHERE id = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_INT);
$stmt = $this->db->prepare("SELECT * FROM contact_messages WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetch();
$contact = $stmt->fetch(\PDO::FETCH_ASSOC);
Logger::info("[DEBUG] Contact data retrieved: " . json_encode($contact));
return $contact ?: null;
} catch (\Exception $e) {
Logger::error("[ERROR] Failed to fetch contact with ID $id: " . $e->getMessage());
return $result ?: null;
} catch (Exception $e) {
Logger::error("ContactModel::getContactById failed: " . $e->getMessage());
ErrorHandler::exception($e);
return null;
}
}
/**
* Add a new contact to the database.
*
* @param array $contactData
* @return bool
*/
public function addContact(array $contactData): bool
{
try {
Logger::info("[DEBUG] Adding new contact: " . json_encode($contactData));
$stmt = $this->db->prepare(
"INSERT INTO contacts (name, email, message) VALUES (:name, :email, :message)"
);
$stmt->bindParam(':name', $contactData['name']);
$stmt->bindParam(':email', $contactData['email']);
$stmt->bindParam(':message', $contactData['message']);
$stmt->execute();
Logger::info("[DEBUG] Contact successfully added.");
return true;
} catch (\Exception $e) {
Logger::error("[ERROR] Failed to add contact: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}
}
/**
* Delete a contact by ID.
*
* Deletes a contact record by ID from `contact_messages`.
*
* @param int $id
* @return bool
*/
public function deleteContactById(int $id): bool
{
try {
Logger::info("[DEBUG] Deleting contact with ID: $id");
$stmt = $this->db->prepare("DELETE FROM contacts WHERE id = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_INT);
$stmt->execute();
Logger::info("[DEBUG] Contact with ID $id successfully deleted.");
return true;
} catch (\Exception $e) {
Logger::error("[ERROR] Failed to delete contact with ID $id: " . $e->getMessage());
$stmt = $this->db->prepare("DELETE FROM contact_messages WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
return $stmt->execute();
} catch (Exception $e) {
Logger::error("ContactModel::deleteContactById failed: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Models;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Service Model

View File

@ -0,0 +1,68 @@
<?php
/**
* File: SubmissionLogModel.php
* Version: 1.0
* Path: /app/Models/SubmissionLogModel.php
* Purpose: Logs every contact form submission attempt for auditing and spam control.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Models;
use PDO;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Class SubmissionLogModel
*
* Handles insertion of contact form submission logs into the database.
*/
class SubmissionLogModel
{
private PDO $db;
/**
* SubmissionLogModel constructor.
*
* @param PDO $db A valid database connection.
*/
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Log a contact form submission attempt.
*
* @param array $data {
* @type string $email The submitted email address.
* @type string|null $phone The submitted phone number.
* @type string|null $ip_address The user's IP address.
* @type string|null $user_agent The user agent string.
* @type bool $was_saved True if saved to contact_messages.
* @type string $reason Classification reason (e.g. 'valid', 'blocked:honeypot').
* }
* @return bool True on success, false on failure.
*/
public function logAttempt(array $data): bool
{
try {
$sql = "INSERT INTO submission_logs (email, phone, ip_address, user_agent, was_saved, reason)
VALUES (:email, :phone, :ip, :agent, :saved, :reason)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':email' => $data['email'],
':phone' => $data['phone'] ?? null,
':ip' => $data['ip_address'] ?? null,
':agent' => $data['user_agent'] ?? null,
':saved' => $data['was_saved'] ? 1 : 0,
':reason' => $data['reason']
]);
} catch (\Throwable $e) {
Logger::error("Failed to log submission attempt.");
ErrorHandler::exception($e);
return false;
}
}
}

View File

@ -2,8 +2,8 @@
namespace WizdomNetworks\WizeWeb\Models;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Service Model

View File

@ -0,0 +1,42 @@
<?php
/**
* File: ContactService.php
* Version: 1.0
* Path: /app/Services/ContactService.php
* Purpose: Sends verification email for contact form submissions
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Services;
use WizdomNetworks\WizeWeb\Utilities\EmailHelper;
use WizdomNetworks\WizeWeb\Utilities\Logger;
class ContactService
{
/**
* Sends a contact email verification message
*
* @param string $email The user's email address
* @param string $code The unique verification code
* @return bool True if email was sent successfully
*/
public static function sendVerificationEmail(string $email, string $code): bool
{
$appUrl = $_ENV['APP_URL'] ?? 'https://wizdom.ca';
$verifyUrl = "$appUrl/verify/$code";
$subject = "Please verify your email address";
$body = <<<HTML
<p>Hello,</p>
<p>Thank you for reaching out to Wizdom Networks. To complete your contact request, please verify your email address by clicking the link below:</p>
<p><a href="$verifyUrl">Verify Email Address</a></p>
<p>If you did not submit this request, you can safely ignore this message.</p>
<p> The Wizdom Networks Team</p>
HTML;
Logger::info("Sending contact verification email to: $email");
return EmailHelper::send($email, $subject, $body);
}
}

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

@ -0,0 +1,115 @@
<?php
/**
* File: NewsletterService.php
* Version: 1.1
* Path: app/Services/
* Purpose: Handles newsletter subscriptions including verification email flow.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Services;
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;
class NewsletterService
{
/**
* Subscribes a user to the newsletter if not already subscribed.
* Sends a verification email with a unique code.
*
* @param string $email
* @param string $ip
* @param string $userAgent
* @return bool True if subscription initiated successfully, false otherwise
*/
public static function subscribeIfNew(string $email, string $ip, string $userAgent): bool
{
try {
$db = Database::getConnection();
// Check if already subscribed
$stmt = $db->prepare("SELECT is_verified FROM subscribers WHERE email = ?");
$stmt->execute([$email]);
$row = $stmt->fetch();
if ($row) {
if ((int) $row['is_verified'] === 1) {
Logger::info("Newsletter signup skipped (already verified): $email");
return false;
} else {
Logger::info("Newsletter re-verification triggered for $email");
// Optionally regenerate and resend code here
}
}
$verificationCode = bin2hex(random_bytes(16));
if ($row) {
// Update existing unverified entry
$stmt = $db->prepare("UPDATE subscribers SET verification_code = ?, ip_address = ?, user_agent = ?, created_at = NOW() WHERE email = ?");
$stmt->execute([$verificationCode, $ip, $userAgent, $email]);
} else {
// Insert new record
$stmt = $db->prepare("
INSERT INTO subscribers (email, verification_code, is_verified, ip_address, user_agent, created_at)
VALUES (?, ?, 0, ?, ?, NOW())
");
$stmt->execute([$email, $verificationCode, $ip, $userAgent]);
}
Logger::info("Newsletter subscription initiated for $email, verification code generated.");
return self::sendVerificationEmail($email, $verificationCode);
} catch (\Throwable $e) {
Logger::error("Newsletter subscription failed for $email: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}
}
/**
* Sends the newsletter verification email.
*
* @param string $email
* @param string $code
* @return bool True if sent successfully, false otherwise
*/
private static function sendVerificationEmail(string $email, string $code): bool
{
try {
$verifyUrl = $_ENV['APP_URL'] . "/verify?code=" . urlencode($code);
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = $_ENV['SMTP_HOST'];
$mail->SMTPAuth = true;
$mail->Username = $_ENV['SMTP_USER'];
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = $_ENV['SMTP_SECURE'] ?? 'tls';
$mail->Port = $_ENV['SMTP_PORT'] ?? 587;
$mail->setFrom($_ENV['MAIL_FROM'], $_ENV['MAIL_FROM_NAME']);
$mail->addAddress($email);
$mail->Subject = 'Confirm your subscription to Wizdom Networks';
$mail->isHTML(true);
$mail->Body = "
<p>Thank you for subscribing to the Wizdom Networks newsletter!</p>
<p>Please click the link below to confirm your subscription:</p>
<p><a href='{$verifyUrl}'>Confirm My Subscription</a></p>
<p>If you did not request this, you can safely ignore this email.</p>
";
$mail->send();
Logger::info("Verification email sent to $email");
return true;
} catch (MailException $e) {
Logger::error("Failed to send verification email to $email: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}
}
}

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,48 @@
<?php
/**
* File: TokenService.php
* Version: 1.0
* Path: app/Services/
* Purpose: Provides generic token generation and validation using HMAC.
*/
namespace WizdomNetworks\WizeWeb\Services;
class TokenService
{
/**
* Generate an HMAC token from a string payload.
*
* @param string $data The string to sign (e.g. email+timestamp).
* @param string $secret Secret key.
* @return string HMAC token.
*/
public function generate(string $data, string $secret): string
{
return hash_hmac('sha256', $data, $secret);
}
/**
* Validate a token against expected data, with optional TTL enforcement.
*
* @param string $data Original payload used to generate token.
* @param string $token Supplied token.
* @param string $secret Secret key used to validate.
* @param int|null $timestamp Unix timestamp used in original payload.
* @param int $ttlSeconds Time-to-live in seconds (default 86400 = 1 day).
* @return bool
*/
public function isValid(string $data, string $token, string $secret, ?int $timestamp = null, int $ttlSeconds = 86400): bool
{
$expected = $this->generate($data, $secret);
if (!hash_equals($expected, $token)) {
return false;
}
if ($timestamp !== null && abs(time() - $timestamp) > $ttlSeconds) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* File: UnsubscribeTokenService.php
* Version: 1.0
* Path: app/Services/
* Purpose: Wrapper for generating and validating unsubscribe tokens using TokenService.
*/
namespace WizdomNetworks\WizeWeb\Services;
use WizdomNetworks\WizeWeb\Services\TokenService;
class UnsubscribeTokenService
{
private TokenService $tokenService;
private string $secret;
private int $ttl;
public function __construct(TokenService $tokenService)
{
$this->tokenService = $tokenService;
$this->secret = $_ENV['UNSUBSCRIBE_SECRET'] ?? 'changeme';
$this->ttl = 86400; // default: 24 hours
}
/**
* Create an unsubscribe token.
*
* @param string $email
* @param int $timestamp
* @return string
*/
public function generate(string $email, int $timestamp): string
{
return $this->tokenService->generate($email . $timestamp, $this->secret);
}
/**
* Validate an unsubscribe token.
*
* @param string $email
* @param int $timestamp
* @param string $token
* @return bool
*/
public function isValid(string $email, int $timestamp, string $token): bool
{
$data = $email . $timestamp;
return $this->tokenService->isValid($data, $token, $this->secret, $timestamp, $this->ttl);
}
}

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,6 +1,6 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
/**
* Utility class to inspect declared classes and autoloading in the application.

View File

@ -0,0 +1,49 @@
<?php
/**
* File: Database.php
* Version: 1.1
* Path: /app/Utilities/Database.php
* Purpose: Provides static method to retrieve PDO database connection using environment variables.
* Project: Wizdom Networks Website & HelpDesk+
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use PDO;
use PDOException;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class Database
{
/**
* Returns a PDO connection using environment variables.
*
* @return PDO
* @throws \Exception
*/
public static function getConnection(): PDO
{
try {
$host = $_ENV['DB_HOST'];
$port = $_ENV['DB_PORT'];
$dbname = $_ENV['DB_NAME'];
$username = $_ENV['DB_USER'];
$password = $_ENV['DB_PASS'];
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
Logger::info("Database connection established successfully.");
return new PDO($dsn, $username, $password, $options);
} catch (\Throwable $e) {
Logger::error("Database connection failed: " . $e->getMessage());
ErrorHandler::exception($e);
throw $e;
}
}
}

View File

@ -0,0 +1,175 @@
<?php
/**
* File: EmailHelper.php
* Version: 2.10
* Path: /app/Utilities/EmailHelper.php
* Purpose: Low-level utility for PHPMailer configuration, rendering, and transport of outbound email.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use WizdomNetworks\WizeWeb\Services\TokenService;
use WizdomNetworks\WizeWeb\Services\UnsubscribeTokenService;
class EmailHelper
{
/**
* Configures PHPMailer with environment settings.
*
* @param PHPMailer $mail
* @return void
*/
public static function configureMailer(PHPMailer $mail): void
{
$mail->isSMTP();
$mail->Host = $_ENV['SMTP_HOST'] ?? 'localhost';
$mail->Port = $_ENV['SMTP_PORT'] ?? 25;
$mail->SMTPAuth = filter_var($_ENV['SMTP_AUTH'] ?? false, FILTER_VALIDATE_BOOLEAN);
$mail->Username = $_ENV['SMTP_USER'] ?? '';
$mail->Password = $_ENV['SMTP_PASS'] ?? '';
$mail->SMTPAutoTLS = filter_var($_ENV['SMTP_AUTO_TLS'] ?? true, FILTER_VALIDATE_BOOLEAN);
$encryption = strtolower(trim($_ENV['SMTP_ENCRYPTION'] ?? ''));
if ($encryption === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} elseif ($encryption === 'tls') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} else {
$mail->SMTPSecure = '';
}
$fromEmail = $_ENV['SMTP_FROM_EMAIL'] ?? 'no-reply@localhost';
$fromName = $_ENV['SMTP_FROM_NAME'] ?? 'Wizdom Mailer';
$mail->setFrom($fromEmail, $fromName);
$mail->SMTPOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
]
];
}
/**
* 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);
$validEmails = [];
foreach ($emails as $email) {
$email = trim($email);
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
$validEmails[] = $email;
}
}
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 = new PHPMailer(true);
self::configureMailer($mail);
$mail->addAddress($to);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->isHTML(true);
$mail->send();
Logger::info("Email sent successfully to $to with subject: $subject");
return true;
} catch (\Throwable $e) {
Logger::error("Email send failed to $to: " . $e->getMessage());
return false;
}
}
/**
* 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 {
$templatePath = __DIR__ . '/../../resources/views/emails/' . $templateName . '.php';
if (!file_exists($templatePath)) {
throw new \Exception("Template not found: $templateName");
}
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 '';
}
}
/**
* Generate a secure unsubscribe link for a subscriber email.
*
* @param string $email
* @return string
*/
public static function buildUnsubscribeLink(string $email): string
{
$ts = time();
$tokenService = new TokenService();
$unsubscribeTokenService = new UnsubscribeTokenService($tokenService);
$sig = $unsubscribeTokenService->generate($email, $ts);
return $_ENV['APP_URL'] . "/unsubscribe?email=" . urlencode($email) . "&ts=$ts&sig=$sig";
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utilities\Logger;
/**
* ErrorHandler Utility
@ -60,6 +60,7 @@ class ErrorHandler
);
Logger::error($message);
http_response_code(500);
echo "$message";
echo "An internal error occurred. Please try again later.";
exit;
}

View File

@ -0,0 +1,73 @@
<?php
/**
* File: HoneypotHelper.php
* Version: 1.0
* Path: /app/Utilities/HoneypotHelper.php
* Purpose: Provides honeypot-based bot protection with JS-injected token verification.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Utilities;
class HoneypotHelper
{
const SESSION_KEY = 'wiz_hpt';
const FIELD_NAME = 'wiz_hpt';
/**
* Start session if needed and generate a honeypot token.
*/
public static function generate(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION[self::SESSION_KEY])) {
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(16));
}
}
/**
* Return the current honeypot token from the session.
*
* @return string|null
*/
public static function getToken(): ?string
{
return $_SESSION[self::SESSION_KEY] ?? null;
}
/**
* Validate the submitted honeypot token and invalidate it after use.
*
* @param string|null $submitted
* @return bool
*/
public static function validate(?string $submitted): bool
{
$expected = $_SESSION[self::SESSION_KEY] ?? null;
unset($_SESSION[self::SESSION_KEY]);
if (!$expected || !$submitted || $submitted !== $expected) {
Logger::warning("Honeypot validation failed. Expected: $expected, Got: $submitted");
return false;
}
return true;
}
/**
* Output the HTML for the honeypot field.
*
* @return string
*/
public static function renderField(): string
{
return sprintf(
'<input type="text" name="%s" id="%s" class="form-control" required style="position: absolute; left: -9999px;" tabindex="-1" autocomplete="off">',
self::FIELD_NAME,
self::FIELD_NAME
);
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Logger Utility

View File

@ -1,9 +1,9 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* NamespaceUpdater Utility

View File

@ -0,0 +1,244 @@
<?php
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utilities\Logger;
class QueueUtility
{
private string $queueDir;
public function __construct(string $queueDir)
{
$this->queueDir = $queueDir;
// Ensure the queue directory exists
if (!is_dir($this->queueDir)) {
mkdir($this->queueDir, 0755, true);
Logger::logInfo("Queue directory created at: $this->queueDir");
}
}
/**
* Add a task to the queue with priority and expiration.
*
* @param string $queueName The name of the queue.
* @param array $task The task to enqueue.
* @param int $priority The priority of the task (lower value = higher priority).
* @param int $ttl Time-to-live in seconds (0 for no expiration).
* @return bool True if the task is added successfully, false otherwise.
*/
public function enqueue(string $queueName, array $task, int $priority = 0, int $ttl = 0): bool
{
$queueFile = $this->queueDir . "/$queueName.queue";
$expiry = $ttl > 0 ? time() + $ttl : 0;
try {
$taskData = serialize(['priority' => $priority, 'expiry' => $expiry, 'task' => $task]);
file_put_contents($queueFile, $taskData . PHP_EOL, FILE_APPEND | LOCK_EX);
Logger::logInfo("Task added to queue: $queueName with priority $priority and expiry $expiry");
return true;
} catch (\Throwable $e) {
Logger::logError("Failed to enqueue task: " . $e->getMessage());
return false;
}
}
/**
* Retrieve and remove the next task from the queue, considering priority and expiration.
*
* @param string $queueName The name of the queue.
* @return array|null The next task, or null if the queue is empty.
*/
public function dequeue(string $queueName): ?array
{
$queueFile = $this->queueDir . "/$queueName.queue";
if (!file_exists($queueFile)) {
Logger::logInfo("Queue file does not exist: $queueFile");
return null;
}
try {
$lines = file($queueFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (empty($lines)) {
unlink($queueFile);
Logger::logInfo("Queue is empty: $queueName");
return null;
}
// Sort tasks by priority and expiration
$tasks = array_map('unserialize', $lines);
usort($tasks, function ($a, $b) {
return $a['priority'] <=> $b['priority'] ?: $a['expiry'] <=> $b['expiry'];
});
// Find the next valid task
$updatedLines = [];
$nextTask = null;
foreach ($tasks as $taskData) {
if ($taskData['expiry'] > 0 && $taskData['expiry'] < time()) {
Logger::logInfo("Skipping expired task in queue: $queueName");
continue;
}
if ($nextTask === null) {
$nextTask = $taskData['task'];
} else {
$updatedLines[] = serialize($taskData);
}
}
file_put_contents($queueFile, implode(PHP_EOL, $updatedLines) . PHP_EOL, LOCK_EX);
return $nextTask;
} catch (\Throwable $e) {
Logger::logError("Failed to dequeue task: " . $e->getMessage());
return null;
}
}
/**
* Retry a failed task by re-adding it to the queue.
*
* @param string $queueName The name of the queue.
* @param array $task The task to retry.
* @param int $priority The priority of the task.
* @param int $retryLimit The maximum number of retries allowed.
* @param int $currentRetry The current retry count (default: 0).
* @return bool True if the task is retried successfully, false otherwise.
*/
public function retryTask(string $queueName, array $task, int $priority = 0, int $retryLimit = 3, int $currentRetry = 0): bool
{
if ($currentRetry >= $retryLimit) {
Logger::logWarning("Task moved to dead letter queue after exceeding retry limit: $queueName");
$this->enqueue("dead_letter_$queueName", $task, $priority);
return false;
}
Logger::logInfo("Retrying task in queue: $queueName, attempt: " . ($currentRetry + 1));
return $this->enqueue($queueName, $task, $priority);
}
/**
* Get the status of a queue.
*
* @param string $queueName The name of the queue.
* @return array|null Queue statistics or null if the queue does not exist.
*/
public function getQueueStats(string $queueName): ?array
{
$queueFile = $this->queueDir . "/$queueName.queue";
if (!file_exists($queueFile)) {
return null;
}
$lines = file($queueFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$tasks = array_map('unserialize', $lines);
return [
'total_tasks' => count($tasks),
'last_modified' => date('Y-m-d H:i:s', filemtime($queueFile)),
'oldest_task' => $tasks[0]['task'] ?? null,
];
}
/**
* Clear all tasks in a queue.
*
* @param string $queueName The name of the queue.
* @return bool True if the queue is cleared successfully, false otherwise.
*/
public function clearQueue(string $queueName): bool
{
$queueFile = $this->queueDir . "/$queueName.queue";
if (!file_exists($queueFile)) {
Logger::logInfo("Queue file does not exist: $queueFile");
return false;
}
try {
unlink($queueFile);
Logger::logInfo("Queue cleared: $queueName");
return true;
} catch (\Throwable $e) {
Logger::logError("Failed to clear queue: " . $e->getMessage());
return false;
}
}
/**
* List all available queues.
*
* @return array List of queue names.
*/
public function listQueues(): array
{
$files = glob($this->queueDir . '/*.queue');
return array_map(function ($file) {
return basename($file, '.queue');
}, $files);
}
/**
* Clear all queues in the directory.
*
* @return bool True if all queues are cleared successfully, false otherwise.
*/
public function clearAllQueues(): bool
{
try {
$files = glob($this->queueDir . '/*.queue');
foreach ($files as $file) {
unlink($file);
}
Logger::logInfo("All queues cleared.");
return true;
} catch (\Throwable $e) {
Logger::logError("Failed to clear all queues: " . $e->getMessage());
return false;
}
}
/**
* Retrieve and remove a batch of tasks from the queue.
*
* @param string $queueName The name of the queue.
* @param int $batchSize The number of tasks to dequeue.
* @return array List of tasks.
*/
public function dequeueBatch(string $queueName, int $batchSize = 10): array
{
$queueFile = "$this->queueDir/$queueName.queue";
if (!file_exists($queueFile)) {
Logger::logInfo("Queue file does not exist: $queueFile");
return [];
}
try {
$lines = file($queueFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (empty($lines)) {
unlink($queueFile);
Logger::logInfo("Queue is empty: $queueName");
return [];
}
$tasks = array_map('unserialize', $lines);
usort($tasks, fn($a, $b) => $a['priority'] <=> $b['priority']);
$batch = array_splice($tasks, 0, $batchSize);
file_put_contents($queueFile, implode(PHP_EOL, array_map('serialize', $tasks)) . PHP_EOL);
return array_map(fn($task) => $task['task'], $batch);
} catch (Throwable $e) {
Logger::logError("Failed to dequeue batch: " . $e->getMessage());
return [];
}
}
}
?>

143
app/Utilities/Response.php Normal file
View File

@ -0,0 +1,143 @@
<?php
namespace WizdomNetworks\WizeWeb\Utilities;
class Response
{
/**
* Send a JSON response.
*
* @param array $data The response data.
* @param int $status HTTP status code (default: 200).
* @param array $headers Additional headers to include in the response.
* @return void
*/
public static function json(array $data, int $statusCode = 200): void
{
if (headers_sent()) {
Logger::logError("Headers already sent. Unable to send JSON response.");
return;
}
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Send an HTML response.
*
* @param string $content The HTML content.
* @param int $status HTTP status code (default: 200).
* @param array $headers Additional headers to include in the response.
* @return void
*/
public static function html(string $content, int $status = 200, array $headers = []): void
{
http_response_code($status);
header('Content-Type: text/html');
self::sendHeaders($headers);
echo $content;
self::logResponse(['content' => $content], $status);
exit;
}
/**
* Send a file download response.
*
* @param string $filePath The file path.
* @param string|null $downloadName The name for the downloaded file (optional).
* @param array $headers Additional headers to include in the response.
* @return void
*/
public static function file(string $filePath, ?string $downloadName = null, array $headers = []): void
{
if (!file_exists($filePath)) {
self::error('File not found.', 404);
}
$downloadName = $downloadName ?? basename($filePath);
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"$downloadName\"");
header('Content-Length: ' . filesize($filePath));
self::sendHeaders($headers);
readfile($filePath);
self::logResponse(['file' => $downloadName], 200);
exit;
}
/**
* Send an error response.
*
* @param string $message The error message.
* @param int $status HTTP status code (default: 500).
* @param array $headers Additional headers to include in the response.
* @return void
*/
public static function error(string $message, int $status = 500, array $headers = []): void
{
self::json(['success' => false, 'message' => $message], $status, $headers);
}
/**
* Predefined response for 400 Bad Request.
*
* @param string $message The error message.
* @return void
*/
public static function badRequest(string $message): void
{
self::error($message, 400);
}
/**
* Predefined response for 404 Not Found.
*
* @param string $message The error message.
* @return void
*/
public static function notFound(string $message): void
{
self::error($message, 404);
}
/**
* Predefined response for 500 Internal Server Error.
*
* @param string $message The error message.
* @return void
*/
public static function serverError(string $message): void
{
self::error($message, 500);
}
/**
* Send custom headers.
*
* @param array $headers Headers to include in the response.
* @return void
*/
private static function sendHeaders(array $headers): void
{
foreach ($headers as $key => $value) {
header("$key: $value");
}
}
/**
* Log the response if debugging is enabled.
*
* @param array $data The response data.
* @param int $status HTTP status code.
* @return void
*/
private static function logResponse(array $data, int $status): void
{
if (getenv('DEBUG') === 'true') {
Logger::logInfo("Response sent with status $status: " . json_encode($data));
}
}
}

View File

@ -1,42 +1,51 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Sanitizer Utility
*
* Provides methods for sanitizing various types of data, including strings, emails, URLs, and arrays.
* Logs sanitized data for debugging and traceability.
*
* Provides secure, traceable input sanitation with modern and recursive handling.
*/
class Sanitizer
{
/**
* Sanitizes a string by removing harmful characters.
*
* @param string $value The string to sanitize.
* @return string The sanitized string.
* Sanitizes a string using modern techniques.
*/
public static function sanitizeString(string $value): string
{
return self::sanitizeInput($value); // alias to avoid deprecated filters
}
/**
* Performs chained sanitation: trim, strip_tags, htmlspecialchars.
*/
public static function sanitizeInput(string $value): string
{
try {
$sanitized = filter_var($value, FILTER_SANITIZE_STRING);
Logger::info("Sanitized string: Original: $value | Sanitized: $sanitized");
$sanitized = htmlspecialchars(strip_tags(trim($value)));
Logger::info("Sanitized input: Original: $value | Sanitized: $sanitized");
return $sanitized;
} catch (\Throwable $e) {
Logger::error("Failed to sanitize string: $value");
Logger::error("Failed to sanitize input: $value");
ErrorHandler::exception($e);
return '';
}
}
/**
* Alias to sanitizeInput() for semantic clarity.
*/
public static function sanitizeChained(string $value): string
{
return self::sanitizeInput($value);
}
/**
* Sanitizes an email address.
*
* @param string $value The email address to sanitize.
* @return string The sanitized email address.
*/
public static function sanitizeEmail(string $value): string
{
@ -53,9 +62,6 @@ class Sanitizer
/**
* Sanitizes a URL.
*
* @param string $value The URL to sanitize.
* @return string The sanitized URL.
*/
public static function sanitizeURL(string $value): string
{
@ -71,15 +77,16 @@ class Sanitizer
}
/**
* Sanitizes an array of strings.
*
* @param array $values The array of strings to sanitize.
* @return array The sanitized array.
* Recursively sanitizes a nested array using sanitizeInput.
*/
public static function sanitizeArray(array $values): array
{
try {
$sanitizedArray = filter_var_array($values, FILTER_SANITIZE_STRING);
$sanitizedArray = array_map(function ($item) {
return is_array($item)
? self::sanitizeArray($item)
: self::sanitizeInput((string)$item);
}, $values);
Logger::info("Sanitized array: Original: " . json_encode($values) . " | Sanitized: " . json_encode($sanitizedArray));
return $sanitizedArray;
} catch (\Throwable $e) {

View File

@ -0,0 +1,115 @@
<?php
/**
* File: SessionHelper.php
* Version: 1.1
* Path: /app/Utilities/SessionHelper.php
* Purpose: Utility to simplify session handling, especially flash messages.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Utilities;
class SessionHelper
{
/**
* Start the PHP session if it hasnt been started yet.
*/
public static function start(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $_SERVER['HTTP_HOST'], // <- ensures subdomain support
'secure' => true, // <- required for HTTPS
'httponly' => true,
'samesite' => 'Lax'
]);
session_start([
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
Logger::info("Session started manually via SessionHelper.");
} else {
Logger::info("Session already active.");
}
Logger::info("Session status: " . session_status());
}
/**
* Set a session variable.
*
* @param string $key
* @param mixed $value
*/
public static function set(string $key, $value): void
{
$_SESSION[$key] = $value;
}
/**
* Get a session variable (does not unset).
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function get(string $key, $default = null)
{
return $_SESSION[$key] ?? $default;
}
/**
* Get and remove a session flash variable.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function flash(string $key, $default = null)
{
$value = $_SESSION[$key] ?? $default;
unset($_SESSION[$key]);
return $value;
}
/**
* Check if a session key is set.
*
* @param string $key
* @return bool
*/
public static function has(string $key): bool
{
return isset($_SESSION[$key]);
}
/**
* Finalize the session and persist data to disk.
*
* @return void
*/
public static function writeClose(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
Logger::info("✅ Session write closed via SessionHelper.");
}
}
/**
* Destroy the session and clear all session data.
*
* @return void
*/
public static function destroy(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION = [];
session_destroy();
Logger::info("Session destroyed via SessionHelper.");
}
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* StructureGenerator Utility

View File

@ -0,0 +1,76 @@
<?php
/**
* File: SubmissionCheck.php
* Version: 1.3
* Purpose: Helper to detect and block repeated or abusive contact form submissions
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use PDO;
use WizdomNetworks\WizeWeb\Utilities\Logger;
class SubmissionCheck
{
private const LOOKBACK_DAYS = 30;
/**
* Evaluates whether a submission is likely spam or abuse.
*
* @param PDO $pdo
* @param string $email
* @param string|null $phone
* @param string|null $ip
* @return array [action: accept|flag|block|notify, reason: string, count: int]
*/
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 $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(':ip1', $ip);
$stmt->bindValue(':ip2', $ip);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
$emailHits = (int)($data['email_hits'] ?? 0);
$phoneHits = (int)($data['phone_hits'] ?? 0);
$ipHits = (int)($data['ip_hits'] ?? 0);
$ipHourly = (int)($data['ip_hourly'] ?? 0);
$totalScore = $emailHits + $phoneHits + $ipHits;
if ($emailHits >= 4 || $phoneHits >= 4 || $ipHits >= 5) {
return ['action' => 'block', 'reason' => 'IP/email/phone threshold exceeded', 'count' => $totalScore];
}
if ($ipHourly >= 3) {
return ['action' => 'flag', 'reason' => 'Multiple submissions from IP in last hour', 'count' => $ipHourly];
}
if ($totalScore >= 6) {
return ['action' => 'notify', 'reason' => 'Cumulative signal from all identifiers', 'count' => $totalScore];
}
if ($emailHits >= 2 || $phoneHits >= 2 || $ipHits >= 2) {
return ['action' => 'flag', 'reason' => 'Repeated pattern detected', 'count' => $totalScore];
}
return ['action' => 'accept', 'reason' => 'accepted', 'count' => $totalScore];
} catch (\Throwable $e) {
Logger::error("SubmissionCheck evaluation failed: " . $e->getMessage());
return ['action' => 'error', 'reason' => 'Evaluation error', 'count' => 0];
}
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* File: UnsubscribeTokenHelper.php
* Version: 1.0
* Path: app/Utilities/
* Purpose: Provides secure token generation and validation for unsubscribe links.
*/
namespace WizdomNetworks\WizeWeb\Utilities;
class UnsubscribeTokenHelper
{
/**
* Generate a secure token for an email + timestamp
*
* @param string $email
* @param int $timestamp
* @return string
*/
public static function generate(string $email, int $timestamp): string
{
$secret = $_ENV['UNSUBSCRIBE_SECRET'] ?? 'changeme';
return hash_hmac('sha256', $email . $timestamp, $secret);
}
/**
* Validate a token with an expiration window (default 24h)
*
* @param string $email
* @param int $timestamp
* @param string $token
* @param int $validForSeconds
* @return bool
*/
public static function isValid(string $email, int $timestamp, string $token, int $validForSeconds = 86400): bool
{
$expected = self::generate($email, $timestamp);
if (!hash_equals($expected, $token)) {
return false;
}
// Check timestamp freshness
return abs(time() - $timestamp) <= $validForSeconds;
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Validator Utility

View File

@ -1,78 +0,0 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
use PDO;
use PDOException;
/**
* Database Utility
*
* A utility for managing database connections and queries.
*
* Integrates logging for connection status and query execution.
*/
class Database
{
/**
* @var PDO|null The PDO instance for database connection.
*/
private ?PDO $connection = null;
/**
* Database constructor.
*
* Initializes the database connection.
*/
public function __construct()
{
$this->connect();
}
/**
* Establishes a connection to the database.
*/
private function connect(): void
{
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $_ENV['DB_HOST'], $_ENV['DB_NAME']);
try {
$this->connection = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASSWORD']);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Logger::info('Database connection established successfully.');
} catch (PDOException $e) {
Logger::error('Database connection failed: ' . $e->getMessage());
throw $e;
}
}
/**
* Executes a query and returns the result.
*
* @param string $query The SQL query to execute.
* @param array $params Parameters for prepared statements (optional).
* @return array The query result.
*/
public function query(string $query, array $params = []): array
{
try {
$stmt = $this->connection->prepare($query);
$stmt->execute($params);
Logger::info('Query executed successfully: ' . $query);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
Logger::error('Query failed: ' . $query . ' | Error: ' . $e->getMessage());
throw $e;
}
}
/**
* Retrieves the PDO connection instance.
*
* @return PDO The PDO instance.
*/
public function getConnection(): PDO
{
return $this->connection;
}
}

View File

@ -1,97 +0,0 @@
<?php
namespace WizdomNetworks\WizeWeb\Utils;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
/**
* Mailer Utility
*
* A utility class for sending emails using PHPMailer.
*
* Integrates logging for email success and failure events.
*/
class Mailer
{
/**
* @var PHPMailer The PHPMailer instance used for sending emails.
*/
protected PHPMailer $mailer;
/**
* Mailer constructor.
*
* Initializes the PHPMailer instance and configures it based on environment variables.
*
* @throws Exception If PHPMailer configuration fails.
*/
public function __construct()
{
try {
$this->mailer = new PHPMailer(true);
$this->configure();
} catch (Exception $e) {
Logger::error('Failed to initialize Mailer: ' . $e->getMessage());
ErrorHandler::exception($e);
throw $e;
}
}
/**
* Configures the PHPMailer instance.
*
* Reads email configuration from environment variables such as MAIL_HOST, MAIL_USER, MAIL_PASSWORD, etc.
*
* @throws Exception If any configuration errors occur.
*/
protected function configure(): void
{
try {
$this->mailer->isSMTP();
$this->mailer->Host = $_ENV['MAIL_HOST'];
$this->mailer->SMTPAuth = true;
$this->mailer->Username = $_ENV['MAIL_USER'];
$this->mailer->Password = $_ENV['MAIL_PASSWORD'];
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$this->mailer->Port = (int) $_ENV['MAIL_PORT'];
$this->mailer->setFrom($_ENV['MAIL_FROM_EMAIL'], $_ENV['MAIL_FROM_NAME']);
Logger::info('Mailer configured successfully.');
} catch (Exception $e) {
Logger::error('Mailer configuration failed: ' . $e->getMessage());
ErrorHandler::exception($e);
throw $e;
}
}
/**
* Sends an email.
*
* @param string $to The recipient's email address.
* @param string $subject The email subject.
* @param string $body The HTML content of the email.
* @param string $altBody The plain-text alternative content of the email (optional).
*
* @return bool True if the email was sent successfully, false otherwise.
*/
public function send(string $to, string $subject, string $body, string $altBody = ''): bool
{
try {
$this->mailer->clearAddresses();
$this->mailer->addAddress($to);
$this->mailer->Subject = $subject;
$this->mailer->Body = $body;
$this->mailer->AltBody = $altBody;
$this->mailer->send();
Logger::info("Email sent successfully to $to with subject: $subject.");
return true;
} catch (Exception $e) {
Logger::error("Failed to send email to $to: " . $e->getMessage());
ErrorHandler::exception($e);
return false;
}
}
}

32
copy-email-utilities.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# Set source and destination paths
SRC_UTILS="../dev.helpdeskplus.ca-main/app/Utilities"
DEST_UTILS="./app/Utils"
SRC_TEMPLATES="../dev.helpdeskplus.ca-main/resources/templates/emails"
DEST_TEMPLATES="./resources/templates/emails"
# Create destination folders if they don't exist
mkdir -p "$DEST_UTILS"
mkdir -p "$DEST_TEMPLATES"
echo "🔁 Copying utility files..."
# List of utility files to copy and update namespaces in
FILES=("EmailUtility.php" "TemplateUtility.php" "QueueUtility.php")
for FILE in "${FILES[@]}"; do
if [ -f "$SRC_UTILS/$FILE" ]; then
echo " 📁 $FILE -> $DEST_UTILS"
sed 's|App\\Utilities|WizdomNetworks\\WizeWeb\\Utilities|g' "$SRC_UTILS/$FILE" > "$DEST_UTILS/$FILE"
else
echo " ⚠️ $FILE not found in $SRC_UTILS"
fi
done
echo "📄 Copying email templates..."
cp -r "$SRC_TEMPLATES"/* "$DEST_TEMPLATES"/
echo "✅ Done. All utilities and templates copied."

113
public/app.log Normal file
View File

@ -0,0 +1,113 @@
[2025-05-08 00:33:46] [INFO]: Bootstrapping application
[2025-05-08 00:33:46] [ERROR]: Route not found: index.php
[2025-05-14 17:15:31] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:15:31] [INFO]: Session status: 2
[2025-05-14 17:15:31] [INFO]: Bootstrapping application
[2025-05-14 17:15:31] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:15:31] [INFO]: Session already active.
[2025-05-14 17:15:31] [INFO]: Session status: 2
[2025-05-14 17:15:31] [INFO]: Session status: 2
[2025-05-14 17:15:31] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:15:31] [INFO]: 🟡 Landing page session before render: []
[2025-05-14 17:15:47] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:15:47] [INFO]: Session status: 2
[2025-05-14 17:15:47] [INFO]: Bootstrapping application
[2025-05-14 17:15:47] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\ContactController::submit
[2025-05-14 17:15:47] [INFO]: Executing controller: ContactController::submit
[2025-05-14 17:15:47] [INFO]: 📦 PHP Session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: John | Sanitized: John
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: Clement | Sanitized: Clement
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: essae@wizdom.ca | Sanitized: essae@wizdom.ca
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: 4168778483 | Sanitized: 4168778483
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: second new subject | Sanitized: second new subject
[2025-05-14 17:15:47] [INFO]: Sanitized input: Original: econd new subject | Sanitized: econd new subject
[2025-05-14 17:15:47] [INFO]: Sanitized input: first_name = John
[2025-05-14 17:15:47] [INFO]: Sanitized input: last_name = Clement
[2025-05-14 17:15:47] [INFO]: Sanitized input: email = essae@wizdom.ca
[2025-05-14 17:15:47] [INFO]: Sanitized input: phone = 4168778483
[2025-05-14 17:15:47] [INFO]: Sanitized input: subject = second new subject
[2025-05-14 17:15:47] [INFO]: Sanitized input: message = econd new subject
[2025-05-14 17:15:47] [INFO]: Sanitized input: ip_address = 10.10.3.1
[2025-05-14 17:15:47] [INFO]: Sanitized input: user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
[2025-05-14 17:15:47] [INFO]: [DEBUG] Validating email address: essae@wizdom.ca
[2025-05-14 17:15:47] [INFO]: Database connection established successfully.
[2025-05-14 17:15:47] [INFO]: ✅ Writing session flag: contact_success = true
[2025-05-14 17:15:47] [INFO]: ✅ Session content before redirect: {"contact_success":true}
[2025-05-14 17:15:47] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:15:47] [INFO]: Session status: 2
[2025-05-14 17:15:47] [INFO]: Bootstrapping application
[2025-05-14 17:15:47] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:15:47] [INFO]: Session already active.
[2025-05-14 17:15:47] [INFO]: Session status: 2
[2025-05-14 17:15:47] [INFO]: Session status: 2
[2025-05-14 17:15:47] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:15:47] [INFO]: 🟡 Landing page session before render: []
[2025-05-14 17:21:55] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:21:55] [INFO]: Session status: 2
[2025-05-14 17:21:55] [INFO]: Bootstrapping application
[2025-05-14 17:21:55] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:21:55] [INFO]: Session already active.
[2025-05-14 17:21:55] [INFO]: Session status: 2
[2025-05-14 17:21:55] [INFO]: Session status: 2
[2025-05-14 17:21:55] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:21:55] [INFO]: 🟡 Landing page session before render: {"contact_success":true}
[2025-05-14 17:22:23] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:22:23] [INFO]: Session status: 2
[2025-05-14 17:22:23] [INFO]: Bootstrapping application
[2025-05-14 17:22:23] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:22:23] [INFO]: Session already active.
[2025-05-14 17:22:23] [INFO]: Session status: 2
[2025-05-14 17:22:23] [INFO]: Session status: 2
[2025-05-14 17:22:23] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:22:23] [INFO]: 🟡 Landing page session before render: []
[2025-05-14 17:22:42] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:22:42] [INFO]: Session status: 2
[2025-05-14 17:22:42] [INFO]: Bootstrapping application
[2025-05-14 17:22:42] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\ContactController::submit
[2025-05-14 17:22:42] [INFO]: Executing controller: ContactController::submit
[2025-05-14 17:22:42] [INFO]: 📦 PHP Session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: Ben | Sanitized: Ben
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: brown | Sanitized: brown
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: code@cloudiq.ca | Sanitized: code@cloudiq.ca
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: 4168778483 | Sanitized: 4168778483
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: second new subject | Sanitized: second new subject
[2025-05-14 17:22:42] [INFO]: Sanitized input: Original: second new subject | Sanitized: second new subject
[2025-05-14 17:22:42] [INFO]: Sanitized input: first_name = Ben
[2025-05-14 17:22:42] [INFO]: Sanitized input: last_name = brown
[2025-05-14 17:22:42] [INFO]: Sanitized input: email = code@cloudiq.ca
[2025-05-14 17:22:42] [INFO]: Sanitized input: phone = 4168778483
[2025-05-14 17:22:42] [INFO]: Sanitized input: subject = second new subject
[2025-05-14 17:22:42] [INFO]: Sanitized input: message = second new subject
[2025-05-14 17:22:42] [INFO]: Sanitized input: ip_address = 10.10.3.1
[2025-05-14 17:22:42] [INFO]: Sanitized input: user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
[2025-05-14 17:22:42] [INFO]: [DEBUG] Validating email address: code@cloudiq.ca
[2025-05-14 17:22:42] [INFO]: Database connection established successfully.
[2025-05-14 17:22:43] [INFO]: ✅ Writing session flag: contact_success = true
[2025-05-14 17:22:43] [INFO]: ✅ Session content before redirect: {"contact_success":true}
[2025-05-14 17:22:43] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:22:43] [INFO]: Session status: 2
[2025-05-14 17:22:43] [INFO]: Bootstrapping application
[2025-05-14 17:22:43] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:22:43] [INFO]: Session already active.
[2025-05-14 17:22:43] [INFO]: Session status: 2
[2025-05-14 17:22:43] [INFO]: Session status: 2
[2025-05-14 17:22:43] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:22:43] [INFO]: 🟡 Landing page session before render: {"contact_success":true}
[2025-05-14 17:27:26] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:27:26] [INFO]: Session status: 2
[2025-05-14 17:27:26] [INFO]: Bootstrapping application
[2025-05-14 17:27:26] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:27:26] [INFO]: Session already active.
[2025-05-14 17:27:26] [INFO]: Session status: 2
[2025-05-14 17:27:26] [INFO]: Session status: 2
[2025-05-14 17:27:26] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:27:26] [INFO]: 🟡 Landing page session before render: []
[2025-05-14 17:31:29] [INFO]: Session started manually via SessionHelper.
[2025-05-14 17:31:29] [INFO]: Session status: 2
[2025-05-14 17:31:29] [INFO]: Bootstrapping application
[2025-05-14 17:31:29] [INFO]: Executing controller: WizdomNetworks\WizeWeb\Controllers\LandingController::index
[2025-05-14 17:31:29] [INFO]: Session already active.
[2025-05-14 17:31:29] [INFO]: Session status: 2
[2025-05-14 17:31:29] [INFO]: Session status: 2
[2025-05-14 17:31:29] [INFO]: 📥 Landing page session ID: 4s18mr50hk6p8mv7f0kbfntl3c
[2025-05-14 17:31:29] [INFO]: 🟡 Landing page session before render: []

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
/* Bootstrap Optimization for Mobile & Desktop Consistency */
/* Main Styling: main.css */
/* Ensure proper grid scaling */
.container {
max-width: 1200px;
margin: auto;
}
/* Navigation Fixes */
.navbar {
padding: 0.5rem 1rem;
}
/* Adjust button sizes for better touchscreen usability */
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
/* Improve form inputs on mobile */
.form-control {
padding: 0.75rem;
font-size: 1rem;
}
/* Ensure text scaling remains readable */
body {
font-size: 1rem;
}
@media (max-width: 768px) {
.container {
padding: 0 15px;
}
.navbar-nav {
text-align: center;
}
}

1
public/assets/images Symbolic link
View File

@ -0,0 +1 @@
/var/www/html/dev.wizdom.ca/public/assets/img

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 423 KiB

View File

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 291 KiB

View File

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

Before

Width:  |  Height:  |  Size: 798 KiB

After

Width:  |  Height:  |  Size: 798 KiB

View File

Before

Width:  |  Height:  |  Size: 942 KiB

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

View File

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 286 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 426 KiB

View File

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 943 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Some files were not shown because too many files have changed in this diff Show More