Fix session handling for contact form success/error alerts and relocate SessionHelper::start in index.php

This commit is contained in:
essae 2025-05-14 23:54:31 -04:00
parent 7a0594d4f5
commit 4e35d36485
13 changed files with 816 additions and 285 deletions

View File

@ -1,9 +1,9 @@
<?php
/**
* File: ContactController.php
* Version: 1.6
* Version: 2.0
* Path: /app/Controllers/ContactController.php
* Purpose: Handles contact form display and submission logic.
* Purpose: Handles contact form submission with error alerts to admins and success to sales.
* Project: Wizdom Networks Website
*/
@ -12,85 +12,103 @@ namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\Validator;
use WizdomNetworks\WizeWeb\Utilities\Response;
use WizdomNetworks\WizeWeb\Utilities\Sanitizer;
use WizdomNetworks\WizeWeb\Utilities\Database;
use WizdomNetworks\WizeWeb\Utilities\EmailHelper;
use WizdomNetworks\WizeWeb\Models\ContactModel;
use Exception;
class ContactController
{
/**
* Display the contact form page.
*
* @return void
*/
public function index(): void
{
View::render('pages/contact');
View::render('pages/landing');
}
/**
* Handle contact form submission.
*
* @return void
*/
public function submit(): void
{
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Response::badRequest('Invalid request method.');
}
{
Logger::info("Executing controller: ContactController::submit");
Logger::info("📦 PHP Session ID: " . session_id());
$data = $_POST;
$requiredFields = ['first_name', 'last_name', 'email', 'phone', 'subject', 'message'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
Response::badRequest("Missing required field: $field");
}
}
try {
$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',
];
// Sanitize input
$firstName = Sanitizer::sanitizeString($data['first_name']);
$lastName = Sanitizer::sanitizeString($data['last_name']);
$email = Sanitizer::sanitizeString($data['email']);
$phone = Sanitizer::sanitizeString($data['phone']);
$subject = Sanitizer::sanitizeString($data['subject']);
$message = Sanitizer::sanitizeString($data['message']);
if (!Validator::IsEmail($email)) {
Response::badRequest('Invalid email address.');
}
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$pdo = (new Database())->getConnection();
$contact = new ContactModel($pdo);
$result = $contact->saveContactForm([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'phone' => $phone,
'subject' => $subject,
'message' => $message,
'ip_address' => $ip,
'user_agent' => $userAgent,
]);
if (!$result) {
Logger::error("Contact form submission failed for email: $email");
Response::serverError('An error occurred while submitting your message. Please try again later.');
}
Response::json([
'success' => true,
'message' => 'Your message has been successfully submitted. Thank you!'
]);
} catch (Exception $e) {
Logger::error("Exception in contact submission: " . $e->getMessage());
Response::serverError('A server error occurred. Please try again later.');
foreach ($formData as $key => $value) {
Logger::info("Sanitized input: {$key} = {$value}");
}
// Validate required fields
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'] = 'An internal error occurred. Please try again later.';
header("Location: /?contact_error=1#contact");
exit;
}
// Save to DB
$db = Database::getConnection();
$contactModel = new ContactModel($db);
$saveSuccess = $contactModel->saveContactForm($formData);
// Send to sales team
$emailSuccess = EmailHelper::sendContactNotification($formData);
// Send confirmation to user
$confirmationSuccess = EmailHelper::sendConfirmationToUser($formData);
if ($saveSuccess && $emailSuccess) {
$_SESSION['contact_success'] = true;
} else {
Logger::error("Form processed but saveSuccess={$saveSuccess}, emailSuccess={$emailSuccess}");
$_SESSION['contact_error'] = 'Your message was received but an internal error occurred. A confirmation may not have been sent.';
EmailHelper::alertAdmins('ContactController::submit - DB or email failure', 'Partial failure', $formData);
}
if (!$confirmationSuccess) {
Logger::error("Confirmation email failed to send to user: {$formData['email']}");
// Don't show user error — it's non-critical
}
Logger::info("✅ Writing session flag: contact_success = true");
Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
header("Location: /?contact_submitted=1#contact");
exit;
} catch (\Throwable $e) {
Logger::error("Fatal error in ContactController::submit: " . $e->getMessage());
EmailHelper::alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
$_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
Logger::info("✅ Writing session flag: catch contact_error = " . $_SESSION['contact_error']);
Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
header("Location: /?contact_error=2#contact");
exit;
}
}
}

View File

@ -1,6 +1,6 @@
<?php
// File: app/Controllers/LandingController.php
// Version: v1.0
// Version: v1.1
// Purpose: Handles landing page rendering for Arsha one-pager
// Project: Wizdom Networks Website
@ -8,12 +8,16 @@ namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\SessionHelper;
class LandingController
{
public function index()
public function index(): void
{
Logger::info("Rendering landing page via LandingController@index");
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 File

@ -1,26 +1,98 @@
<?php
/**
* File: ContactModel.php
* Version: 2.1
* Path: /app/Models/ContactModel.php
* Purpose: Manages saving and retrieving contact records from both legacy and full form submissions.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Models;
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.
*
* @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
) VALUES (
:first_name, :last_name, :email, :phone, :subject, :message,
:ip_address, :user_agent
)
");
$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']);
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
@ -28,56 +100,21 @@ class ContactModel
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
@ -85,65 +122,13 @@ class ContactModel
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;
}
}
/**
* Save a full contact form submission to contact_messages table.
*
* @param array $data
* @return bool
*/
public function saveContactForm(array $data): bool
{
try {
$sql = "INSERT INTO contact_messages (
first_name,
last_name,
email,
phone,
subject,
message,
ip_address,
user_agent
) VALUES (
:first_name,
:last_name,
:email,
:phone,
:subject,
:message,
:ip_address,
:user_agent
)";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':first_name', $data['first_name']);
$stmt->bindParam(':last_name', $data['last_name']);
$stmt->bindParam(':email', $data['email']);
$stmt->bindParam(':phone', $data['phone']);
$stmt->bindParam(':subject', $data['subject']);
$stmt->bindParam(':message', $data['message']);
$stmt->bindParam(':ip_address', $data['ip_address']);
$stmt->bindParam(':user_agent', $data['user_agent']);
return $stmt->execute();
} catch (\Exception $e) {
Logger::error("Failed to save contact form: " . $e->getMessage());
return false;
}
}
}

View File

@ -1,78 +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;
/**
* 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.
* Returns a PDO connection using environment variables.
*
* Initializes the database connection.
* @return PDO
* @throws \Exception
*/
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_PASS']);
$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
public static function getConnection(): PDO
{
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());
$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;
}
}
/**
* Retrieves the PDO connection instance.
*
* @return PDO The PDO instance.
*/
public function getConnection(): PDO
{
return $this->connection;
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* File: EmailHelper.php
* Version: 2.4
* Path: /app/Utilities/EmailHelper.php
* Purpose: Sends contact confirmations, sales notifications, and admin alerts with proper formatting and logic.
* Project: Wizdom Networks Website
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
class EmailHelper
{
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
]
];
}
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;
}
private static function buildContactHtmlBody(array $data): string
{
return "
<strong>Name:</strong> {$data['first_name']} {$data['last_name']}<br>
<strong>Email:</strong> {$data['email']}<br>
<strong>Phone:</strong> {$data['phone']}<br>
<strong>Subject:</strong> {$data['subject']}<br>
<strong>Message:</strong><br>
<pre style='white-space:pre-wrap;'>{$data['message']}</pre>
";
}
private static function buildErrorReportHtml(string $context, string $errorMessage, $data = []): string
{
if (is_string($data)) {
$decoded = json_decode($data, true);
$data = is_array($decoded) ? $decoded : ['raw_data' => $data];
}
$body = "
<strong>Context:</strong> {$context}<br>
<strong>Error Message:</strong><br>
<pre style='white-space:pre-wrap;color:#8b0000;'>{$errorMessage}</pre>
";
if (!empty($data)) {
$body .= "<hr><strong>Associated Data:</strong><br><ul>";
foreach ($data as $key => $value) {
$safeKey = htmlspecialchars($key);
$safeValue = nl2br(htmlspecialchars((string)$value));
$body .= "<li><strong>{$safeKey}:</strong> {$safeValue}</li>";
}
$body .= "</ul>";
}
return $body;
}
private static function buildSalesHtmlBody(array $data): string
{
$submittedAt = date('Y-m-d H:i:s');
return "
<p><strong>New contact submission received</strong> on {$submittedAt}.</p>
<ul>
<li><strong>Name:</strong> {$data['first_name']} {$data['last_name']}</li>
<li><strong>Email:</strong> {$data['email']}</li>
<li><strong>Phone:</strong> {$data['phone']}</li>
<li><strong>Subject:</strong> {$data['subject']}</li>
<li><strong>Message:</strong><br><pre style='white-space:pre-wrap;'>{$data['message']}</pre></li>
</ul>
<hr>
<p style='font-size: 0.9em; color: #888;'>IP: {$data['ip_address']}<br>User-Agent: {$data['user_agent']}</p>
";
}
private static function buildConfirmationHtmlBody(array $data): string
{
$submittedAt = date('Y-m-d H:i:s');
return "
<p>Hi {$data['first_name']},</p>
<p>Thank you for contacting Wizdom Networks. This message confirms that we received your inquiry on <strong>{$submittedAt}</strong>.</p>
<ul>
<li><strong>Subject:</strong> {$data['subject']}</li>
<li><strong>Message:</strong><br><pre style='white-space:pre-wrap;'>{$data['message']}</pre></li>
</ul>
<p>Well be in touch shortly. If its urgent, call us at <strong>416-USE-WISE</strong>.</p>
<p style='font-size: 0.9em; color: #888;'>IP: {$data['ip_address']}<br>User-Agent: {$data['user_agent']}</p>
";
}
public static function sendContactNotification(array $data): bool
{
try {
$mail = new PHPMailer(true);
self::configureMailer($mail);
$recipients = self::parseRecipients($_ENV['SALES_EMAILS'] ?? '');
foreach ($recipients as $email) {
$mail->addAddress($email);
}
if (empty($mail->getToAddresses())) {
Logger::error("EmailHelper: No valid SALES_EMAILS configured.");
return false;
}
$mail->isHTML(true);
$mail->Subject = 'New Contact Form Submission';
$mail->Body = self::buildSalesHtmlBody($data);
$mail->send();
return true;
} catch (Exception $e) {
Logger::error("Email send failure to sales: " . $e->getMessage());
self::alertAdmins("sendContactNotification", $e->getMessage(), $data);
return false;
}
}
public static function sendConfirmationToUser(array $data): bool
{
try {
$mail = new PHPMailer(true);
self::configureMailer($mail);
$mail->addAddress($data['email'], "{$data['first_name']} {$data['last_name']}");
$mail->isHTML(true);
$mail->Subject = 'Your Wizdom Networks Contact Form Submission';
$mail->Body = self::buildConfirmationHtmlBody($data);
$mail->send();
return true;
} catch (Exception $e) {
Logger::error("Email send failure to user: " . $e->getMessage());
self::alertAdmins("sendConfirmationToUser", $e->getMessage(), $data);
return false;
}
}
/**
* Sends a system alert to ADMIN_EMAILS with error context and message.
*
* @param string $context
* @param string $errorMessage
* @param array|string $data Data array or JSON string for the report
* @return void
*/
public static function alertAdmins(string $context, string $errorMessage, $data = []): void
{
try {
$mail = new PHPMailer(true);
self::configureMailer($mail);
$recipients = self::parseRecipients($_ENV['ADMIN_EMAILS'] ?? '');
foreach ($recipients as $email) {
$mail->addAddress($email);
}
if (empty($mail->getToAddresses())) {
Logger::error("EmailHelper: No valid ADMIN_EMAILS configured.");
return;
}
$mail->isHTML(true);
$mail->Subject = "[System Alert] Error in {$context}";
$mail->Body = self::buildErrorReportHtml($context, $errorMessage, $data);
$mail->send();
} catch (Exception $e) {
Logger::error("EmailHelper::alertAdmins failed: " . $e->getMessage());
}
}
}

View File

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

View File

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

View File

@ -1,2 +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: []

View File

@ -2195,6 +2195,7 @@ section,
.contact .php-email-form input[type=text],
.contact .php-email-form input[type=email],
.contact .php-email-form input[type=tel],
.contact .php-email-form textarea {
font-size: 14px;
padding: 10px 15px;
@ -2207,12 +2208,14 @@ section,
.contact .php-email-form input[type=text]:focus,
.contact .php-email-form input[type=email]:focus,
.contact .php-email-form input[type=tel]:focus,
.contact .php-email-form textarea:focus {
border-color: var(--accent-color);
}
.contact .php-email-form input[type=text]::placeholder,
.contact .php-email-form input[type=email]::placeholder,
.contact .php-email-form input[type=tel]::placeholder,
.contact .php-email-form textarea::placeholder {
color: color-mix(in srgb, var(--default-color), transparent 70%);
}

View File

@ -1,3 +1,12 @@
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
if (params.get('contact_submitted') || params.get('contact_error')) {
const contactSection = document.getElementById('contact');
if (contactSection) {
contactSection.scrollIntoView({ behavior: 'smooth' });
}
}
});
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('.php-email-form');
if (!form) return;

View File

@ -1,52 +1,53 @@
<?php
/**
* File: public/index.php
* Version: v1.4
* Purpose: Application entry point for Arsha one-pager with early session start
* Project: Wizdom Networks Website
*/
// Enable strict error reporting
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// File: public/index.php
// Version: v1.3
// Purpose: Application entry point with Arsha one-pager routing
// Project: Wizdom Networks Website
// Autoload classes via Composer
require_once __DIR__ . '/../vendor/autoload.php';
// Load environment variables
use Dotenv\Dotenv;
use WizdomNetworks\WizeWeb\Core\Router;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
use WizdomNetworks\WizeWeb\Controllers\LandingController;
use WizdomNetworks\WizeWeb\Controllers\ContactController; //
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
// Start session before any router logic
use WizdomNetworks\WizeWeb\Utilities\SessionHelper;
SessionHelper::start();
// Import core system utilities
use WizdomNetworks\WizeWeb\Core\Router;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
// Import controllers
use WizdomNetworks\WizeWeb\Controllers\LandingController;
use WizdomNetworks\WizeWeb\Controllers\ContactController;
// Boot the application
Logger::info("Bootstrapping application");
// Initialize router and define routes
$router = new Router();
// Arsha landing routes
// Landing page routes
$router->add('', LandingController::class, 'index');
$router->add('/', LandingController::class, 'index');
$router->add('index.php', LandingController::class, 'index');
// Contact form
$router->add('/contact', ContactController::class, 'index', 'GET'); // Show contact form
$router->add('/contact', ContactController::class, 'submit', 'POST'); // Handle submission
// Optional: fallback for /contact without leading slash (rare case)
// $router->add('contact', ContactController::class, 'submit', 'POST');
// Debug block — safe to leave commented
/*
echo "<pre>";
echo "REQUEST_URI: " . $_SERVER['REQUEST_URI'] . "\n";
echo "Parsed path: " . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . "\n";
echo "Trimmed: " . trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') . "\n";
echo "</pre>";
exit;
*/
// Contact form routes
$router->add('/contact', ContactController::class, 'index', 'GET');
$router->add('/contact', ContactController::class, 'submit', 'POST');
// Dispatch the incoming request
$requestedPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$router->dispatch($requestedPath);

View File

@ -1,4 +1,7 @@
<?php
// use WizdomNetworks\WizeWeb\Utilities\SessionHelper;
// SessionHelper::start();
// ============================================
// File: arsha.php
// Version: 1.2

View File

@ -9,7 +9,11 @@
* Usage: Rendered via LandingController::index()
* ============================================
*/
echo "<!-- SESSION: " . json_encode($_SESSION) . " -->";
?>
<!-- ======= Hero Section ======= -->
<section id="hero" class="hero section dark-background">
<div class="container">
@ -709,17 +713,43 @@
</div>
<div class="col-lg-7">
<form action="/contact" method="POST" class="php-email-form">
<?php
$hasSuccess = $_SESSION['contact_success'] ?? false;
$hasError = $_SESSION['contact_error'] ?? null;
unset($_SESSION['contact_success']);
unset($_SESSION['contact_error']);
if ($hasSuccess): ?>
<div class="alert alert-success"> Thank you! Your message has been received. We'll be in touch shortly.</div>
<?php elseif (!empty($hasError)): ?>
<div class="alert alert-danger">⚠️ <?= htmlspecialchars($hasError) ?></div>
<?php endif; ?>
<form action="/contact" method="POST" class="needs-validation" novalidate>
<div class="row">
<div class="col-md-6">
<label for="first_name" class="pb-2">First Name</label>
<input type="text" name="first_name" id="first_name" class="form-control" required>
<div class="invalid-feedback">First name is required.</div>
</div>
<div class="col-md-6">
<label for="last_name" class="pb-2">Last Name</label>
<input type="text" name="last_name" id="last_name" class="form-control" required>
<div class="invalid-feedback">Last name is required.</div>
</div>
</div>
@ -727,34 +757,34 @@
<div class="col-md-6">
<label for="email" class="pb-2">Your Email</label>
<input type="email" name="email" id="email" class="form-control" required>
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
<div class="col-md-6">
<label for="phone" class="pb-2">Phone Number <small class="text-muted">(required to qualify submissions)</small></label>
<input type="tel" name="phone" id="phone" class="form-control" required>
<div class="invalid-feedback">Phone number is required.</div>
</div>
</div>
<div class="row mt-3">
<div class="form-group mt-3">
<label for="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
class="form-control"
required
placeholder="Subject of your inquiry"
>
<div class="col-12">
<label for="subject" class="pb-2">Subject</label>
<input type="text" name="subject" id="subject" class="form-control" required placeholder="Subject of your inquiry">
<div class="invalid-feedback">Subject is required.</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="message" class="pb-2">Message</label>
<textarea name="message" id="message" rows="5" class="form-control" required></textarea>
<div class="invalid-feedback">Please enter your message.</div>
</div>
</div>
<div class="text-center mt-4">
<button type="submit">Send Message</button>
<button type="submit" class="btn btn-primary">Send Message</button>
</div>
</form>
@ -902,3 +932,18 @@
</footer><!-- End Footer -->
<script>
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>