Compare commits
16 Commits
master
...
integrate-
| Author | SHA1 | Date |
|---|---|---|
|
|
70105e095c | |
|
|
5ec5195d89 | |
|
|
cf146973f2 | |
|
|
e09f763db3 | |
|
|
fc4e8ae851 | |
|
|
a1c25d4885 | |
|
|
1d96ccd3c1 | |
|
|
4e35d36485 | |
|
|
7a0594d4f5 | |
|
|
b48a5f8e0c | |
|
|
761c41d3bb | |
|
|
e4ff1f0a59 | |
|
|
90b7b0b785 | |
|
|
c309fa1eee | |
|
|
a258493698 | |
|
|
0dea9b9c7c |
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,70 +1,138 @@
|
|||
<?php
|
||||
/**
|
||||
* ============================================
|
||||
* File: Router.php
|
||||
* Path: /app/Core/
|
||||
* Purpose: Core router handling HTTP method–specific 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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* File: ResendVerificationService.php
|
||||
* Version: 1.4
|
||||
* Path: /app/Services/ResendVerificationService.php
|
||||
* Purpose: Encapsulates logic for validating, logging, and processing verification email resends.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Services\EmailService;
|
||||
use WizdomNetworks\WizeWeb\Services\VerificationService;
|
||||
|
||||
class ResendVerificationService
|
||||
{
|
||||
/**
|
||||
* @var EmailService Handles email composition and delivery.
|
||||
*/
|
||||
private EmailService $emailService;
|
||||
|
||||
/**
|
||||
* @var VerificationService Handles generation and storage of verification codes.
|
||||
*/
|
||||
private VerificationService $verificationService;
|
||||
|
||||
/**
|
||||
* Constructor initializes email and verification services.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->emailService = new EmailService();
|
||||
$this->verificationService = new VerificationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resend a verification email for a given type and address.
|
||||
* Performs rate limiting checks and logs the attempt if permitted.
|
||||
* Generates and assigns a new verification code and triggers an email send.
|
||||
*
|
||||
* @param string $type Either 'contact' or 'newsletter'
|
||||
* @param string $email Email address to resend to
|
||||
* @return array ['success' => bool, 'message' => string] Outcome and message for user feedback
|
||||
*/
|
||||
public function attemptResend(string $type, string $email): array
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
|
||||
// Rate limit: no more than 3 per day
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 1 DAY");
|
||||
$stmt->execute([$email, $type]);
|
||||
if ((int)$stmt->fetchColumn() >= 3) {
|
||||
return ['success' => false, 'message' => 'You have reached the daily resend limit. Please try again tomorrow.'];
|
||||
}
|
||||
|
||||
// Rate limit: no more than 1 every 5 minutes
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 5 MINUTE");
|
||||
$stmt->execute([$email, $type]);
|
||||
if ((int)$stmt->fetchColumn() > 0) {
|
||||
return ['success' => false, 'message' => 'You must wait a few minutes before requesting another verification email.'];
|
||||
}
|
||||
|
||||
// Log attempt
|
||||
$stmt = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
|
||||
$stmt->execute([
|
||||
$email,
|
||||
$type,
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
$code = $this->verificationService->generateCode();
|
||||
$expiry = $this->verificationService->getExpirationTime();
|
||||
|
||||
if ($type === 'newsletter') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
return ['success' => false, 'message' => 'Email is already verified or not found.'];
|
||||
}
|
||||
|
||||
$this->verificationService->assignCodeToRecord('subscribers', $row['id'], $code, $expiry);
|
||||
$this->emailService->sendVerificationEmail($email, $code, 'verify_newsletter');
|
||||
} elseif ($type === 'contact') {
|
||||
$stmt = $db->prepare("SELECT id, is_verified FROM contact_messages WHERE email = ? ORDER BY created_at DESC LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || (int)$row['is_verified'] === 1) {
|
||||
return ['success' => false, 'message' => 'Email is already verified or not found.'];
|
||||
}
|
||||
|
||||
$this->verificationService->assignCodeToRecord('contact_messages', $row['id'], $code, $expiry);
|
||||
$this->emailService->sendVerificationEmail($email, $code, 'verify_contact');
|
||||
} else {
|
||||
return ['success' => false, 'message' => 'Invalid verification type specified.'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'We just sent you a new verification link.'];
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("ResendVerificationService::attemptResend exception: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => 'An unexpected error occurred.'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
/**
|
||||
* File: VerificationService.php
|
||||
* Version: 1.0
|
||||
* Path: /app/Services/VerificationService.php
|
||||
* Purpose: Manages generation, storage, expiration, and removal of email verification codes.
|
||||
* Project: Wizdom Networks Website
|
||||
*/
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Services;
|
||||
|
||||
use WizdomNetworks\WizeWeb\Utilities\Database;
|
||||
use WizdomNetworks\WizeWeb\Utilities\Logger;
|
||||
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
class VerificationService
|
||||
{
|
||||
private const CODE_BYTES = 16;
|
||||
private const EXPIRATION_INTERVAL = '+72 hours';
|
||||
|
||||
/**
|
||||
* Generates a secure verification code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateCode(): string
|
||||
{
|
||||
return bin2hex(random_bytes(self::CODE_BYTES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration timestamp for a verification code.
|
||||
*
|
||||
* @return string MySQL-compatible datetime string
|
||||
*/
|
||||
public function getExpirationTime(): string
|
||||
{
|
||||
return (new DateTime(self::EXPIRATION_INTERVAL))->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a verification code to a contact or subscriber record.
|
||||
*
|
||||
* @param string $table Table name (e.g., 'subscribers', 'contact_messages')
|
||||
* @param int $id Record ID
|
||||
* @param string $code Verification code
|
||||
* @param string $expiresAt Expiration timestamp
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function assignCodeToRecord(string $table, int $id, string $code, string $expiresAt): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
|
||||
return $stmt->execute([$code, $expiresAt, $id]);
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to assign verification code to {$table} ID {$id}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes expired verification codes from a table.
|
||||
*
|
||||
* @param string $table Table name (e.g., 'subscribers', 'contact_messages')
|
||||
* @return int Number of rows deleted
|
||||
*/
|
||||
public function deleteExpiredCodes(string $table): int
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE verification_expires_at IS NOT NULL AND verification_expires_at < NOW()");
|
||||
$stmt->execute();
|
||||
return $stmt->rowCount();
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to clear expired codes in {$table}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the verification code from a specific record.
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int $id Record ID
|
||||
* @return bool
|
||||
*/
|
||||
public function clearCode(string $table, int $id): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
} catch (Exception $e) {
|
||||
Logger::error("Failed to clear verification code for {$table} ID {$id}: " . $e->getMessage());
|
||||
ErrorHandler::exception($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WizdomNetworks\WizeWeb\Utils;
|
||||
namespace WizdomNetworks\WizeWeb\Utilities;
|
||||
|
||||
/**
|
||||
* Utility class to inspect declared classes and autoloading in the application.
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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 hasn’t 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
||||
|
|
@ -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: []
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/var/www/html/dev.wizdom.ca/public/assets/img
|
||||
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 798 KiB After Width: | Height: | Size: 798 KiB |
|
Before Width: | Height: | Size: 942 KiB After Width: | Height: | Size: 942 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 943 KiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 462 B |