Integrate EmailUtility, SubmissionCheck, and Response into ContactController; finalize contact form logic and template handling

This commit is contained in:
essae 2025-05-12 15:06:39 -04:00
parent 90b7b0b785
commit e4ff1f0a59
9 changed files with 1038 additions and 44 deletions

View File

@ -4,7 +4,7 @@
* File: ContactController.php * File: ContactController.php
* Path: /app/Controllers/ContactController.php * Path: /app/Controllers/ContactController.php
* Purpose: Handles form submissions from the Arsha contact form * Purpose: Handles form submissions from the Arsha contact form
* Version: 1.0 * Version: 1.1
* Author: Wizdom Networks * Author: Wizdom Networks
* Usage: Routed via Router to handle POST /contact * Usage: Routed via Router to handle POST /contact
* ============================================ * ============================================
@ -12,9 +12,15 @@
namespace WizdomNetworks\WizeWeb\Controllers; namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Utils\Logger;
use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Core\View; use WizdomNetworks\WizeWeb\Core\View;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
use WizdomNetworks\WizeWeb\Utilities\EmailUtility;
use WizdomNetworks\WizeWeb\Utilities\Sanitizer;
use WizdomNetworks\WizeWeb\Utilities\Validator;
use WizdomNetworks\WizeWeb\Utilities\Response;
use WizdomNetworks\WizeWeb\Utilities\SubmissionCheck;
use WizdomNetworks\WizeWeb\Utilities\Database;
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\Exception;
@ -23,24 +29,31 @@ class ContactController
public function submit(): void public function submit(): void
{ {
try { try {
// Sanitize and validate input $firstName = Sanitizer::sanitizeString($_POST['first_name'] ?? '');
$firstName = trim($_POST['first_name'] ?? ''); $lastName = Sanitizer::sanitizeString($_POST['last_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? ''); $email = Sanitizer::sanitizeEmail($_POST['email'] ?? '');
$email = trim($_POST['email'] ?? ''); $phone = Sanitizer::sanitizePhone($_POST['phone'] ?? '');
$phone = trim($_POST['phone'] ?? ''); $message = Sanitizer::sanitizeText($_POST['message'] ?? '');
$message = trim($_POST['message'] ?? '');
if (!$firstName || !$lastName || !$email || !$phone || !$message) { if (!$firstName || !$lastName || !$email || !$phone || !$message) {
throw new \Exception("All fields except phone must be filled out."); Response::jsonError('All fields must be filled out.');
return;
} }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { if (!Validator::isValidEmail($email)) {
throw new \Exception("Invalid email address."); Response::jsonError('Please provide a valid email address.');
return;
} }
// Store in database // Check if email has submitted recently
$pdo = new \PDO($_ENV['DB_DSN'], $_ENV['DB_USER'], $_ENV['DB_PASS']); if (SubmissionCheck::isDuplicateContactEmail($email)) {
$stmt = $pdo->prepare("INSERT INTO contact_messages (first_name, last_name, email, phone, message, ip_address, user_agent) $lastDate = SubmissionCheck::getLastContactSubmissionDate($email);
Response::jsonError("This email already submitted a message on $lastDate. Please wait a week before sending another or contact us directly.");
return;
}
$db = Database::getConnection();
$stmt = $db->prepare("INSERT INTO contact_messages (first_name, last_name, email, phone, message, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?)"); VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([ $stmt->execute([
$firstName, $firstName,
@ -54,35 +67,23 @@ class ContactController
Logger::info("Contact form submitted by $firstName $lastName <$email>"); Logger::info("Contact form submitted by $firstName $lastName <$email>");
// Email notification // Send internal notification email
$mail = new PHPMailer(true); EmailUtility::sendInternalContactAlert([
$mail->isSMTP(); 'first_name' => $firstName,
$mail->Host = $_ENV['SMTP_HOST']; 'last_name' => $lastName,
$mail->Port = $_ENV['SMTP_PORT']; 'email' => $email,
$mail->SMTPAuth = $_ENV['SMTP_AUTH'] === 'true'; 'phone' => $phone,
$mail->SMTPSecure = $_ENV['SMTP_ENCRYPTION'] !== 'none' ? $_ENV['SMTP_ENCRYPTION'] : ''; 'message' => $message
$mail->Username = $_ENV['SMTP_USERNAME']; ]);
$mail->Password = $_ENV['SMTP_PASSWORD'];
$mail->setFrom($_ENV['SMTP_FROM_EMAIL'], $_ENV['SMTP_FROM_NAME']);
$mail->addAddress($_ENV['SALES_EMAILS'] ?? $_ENV['ADMIN_EMAILS']);
$mail->Subject = "New Contact Message from $firstName $lastName"; // Send confirmation to user
$mail->Body = "You received a message from: \n\n" EmailUtility::sendContactConfirmation($email, $firstName);
. "Name: $firstName $lastName\n"
. "Email: $email\n"
. "Phone: $phone\n"
. "Message:\n$message\n";
$mail->send(); Response::jsonSuccess('Thank you. We will be in touch shortly.');
http_response_code(200);
echo json_encode(['success' => true, 'message' => 'Thank you. We will be in touch.']);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Logger::error("Contact form error: " . $e->getMessage()); ErrorHandler::exception($e);
ErrorHandler::handleException($e); Response::jsonError('An internal error occurred. Please try again later.');
http_response_code(400);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
} }
} }
} }

View File

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

View File

@ -0,0 +1,393 @@
<?php
namespace WizdomNetworks\WizeWeb\Utilities;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use PHPMAILER\PHPMAILER\SMTP;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\Database;
class EmailUtility
{
private static QueueUtility $queueUtility;
/**
* Initialize the EmailUtility with the QueueUtility instance.
*
* @param QueueUtility $queueUtility The queue utility instance for managing email queues.
*/
public static function initialize(QueueUtility $queueUtility): void
{
self::$queueUtility = $queueUtility;
}
/**
* Retrieve and validate email configuration from environment variables.
*
* @return array The validated email configuration settings.
* @throws \RuntimeException If required email configurations are missing.
*/
private static function getConfig(): array
{
if ( $_ENV['SMTP_AUTH'] === false ) {
$config = [
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
'port' => $_ENV['SMTP_PORT'] ?? '25',
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
'auth' => false
];
foreach (['host', 'port', 'from_email'] as $field) {
if (empty($config[$field])) {
Logger::logError("Missing email configuration: $field");
throw new \RuntimeException("Missing email configuration: $field");
}
}
}
else {
$config = [
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
'username' => $_ENV['SMTP_USERNAME'] ?? null,
'password' => $_ENV['SMTP_PASSWORD'] ?? null,
'port' => $_ENV['SMTP_PORT'] ?? '25',
'encryption' => $_ENV['SMTP_ENCRYPTION'] ?? 'none',
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
'smtpsecure' => $_ENV['SMTP_AUTH'],
'auth' => true,
'autotls' => true
];
foreach (['host', 'port', 'username', 'password', 'from_email'] as $field) {
if (empty($config[$field])) {
Logger::logError("Missing email configuration: $field");
throw new \RuntimeException("Missing email configuration: $field");
}
}
}
return $config;
}
/**
* Render an email template with dynamic data.
*
* @param string $templatePath The path to the email template file.
* @param array $data Key-value pairs for template placeholders.
* @return string The rendered email content.
*/
public static function renderTemplate(string $templatePath, array $data): string
{
if (!file_exists($templatePath)) {
Logger::logError("Email template not found: $templatePath");
return '';
}
$content = file_get_contents($templatePath);
foreach ($data as $key => $value) {
$content = str_replace("{{{$key}}}", $value, $content);
}
return $content;
}
/**
* Log email status into the database.
*
* @param string $recipient The recipient email address.
* @param string $status The status of the email (e.g., 'queued', 'sent', 'failed').
* @param string|null $errorMessage An optional error message.
* @return void
*/
private static function logEmailStatus(string $recipient, string $status, ?string $errorMessage = null): void
{
try {
$db = Database::getInstance();
$query = "INSERT INTO email_status (recipient, status, error_message, created_at) VALUES (:recipient, :status, :error_message, NOW())";
$params = [
':recipient' => $recipient,
':status' => $status,
':error_message' => $errorMessage,
];
$db->executeQuery($query, $params);
} catch (\Throwable $e) {
Logger::logError("Failed to log email status: " . $e->getMessage());
}
}
/**
* Notify admin or sales team via email.
*
* @param string $emailType The type of notification (e.g., 'admin', 'sales').
* @param string $subject The email subject.
* @param string $templatePath Path to the notification template.
* @param array $templateData Data for the template placeholders.
* @return void
*/
public static function notifyTeam(string $emailType, string $subject, string $templatePath, array $templateData): void
{
$recipients = $emailType === 'admin' ? explode(',', $_ENV['ADMIN_EMAILS']) : explode(',', $_ENV['SALES_EMAILS']);
foreach ($recipients as $recipient) {
$recipient = trim($recipient);
if (!self::sendEmail($recipient, $subject, $templatePath, $templateData)) {
Logger::logError("Failed to send $emailType notification to: $recipient");
}
}
}
/**
* Send an email with enhanced error categorization.
*
* @param string $recipient Recipient email address.
* @param string $subject Email subject.
* @param string $templatePath Path to the email template.
* @param array $templateData Data to replace placeholders in the template.
* @param array $options Optional configurations (e.g., CC, BCC).
* @param int $retryLimit The maximum number of retries for transient failures.
* @return bool Returns true on success, false otherwise.
*/
public static function sendEmail(string $recipient, string $subject, string $templatePath, array $templateData = [], array $options = [], int $retryLimit = 3): bool
{
$mail = new PHPMailer(true);
$config = self::getConfig();
$retryCount = 0;
while ($retryCount <= $retryLimit) {
try {
$mail->isSMTP();
$mail->SMTPAutoTLS = false;
$mail->SMTPAuth = false;
/* If authentication is enabled setup the connection */
if ( $config['auth'] === 'true' ){
$mail->SMTPAuth = $config['auth'];
$mail->Username = $config['username'];
$mail->Password = $config['password'];
$mail->SMTPSecure = $config['encryption'];
$mail->SMTPAutoTLS = $config['autotls'];
}
$mail->Host = $config['host'];
$mail->Port = $config['port'];
/******************************
$mail->SMTPDebug = $_ENV['APP_ENV'] === 'development' ? 2 : 0;
/$mail->Debugoutput = function ($message, $level) {
Logger::logInfo("SMTP Debug [$level]: $message");
};
*******************************/
$mail->setFrom($config['from_email'], $config['from_name']);
$mail->addAddress($recipient);
if (!empty($options['cc'])) {
foreach ((array)$options['cc'] as $cc) {
$mail->addCC($cc);
}
}
if (!empty($options['bcc'])) {
foreach ((array)$options['bcc'] as $bcc) {
$mail->addBCC($bcc);
}
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = self::renderTemplate($templatePath, $templateData);
$mail->send();
Logger::logInfo("Email sent to $recipient with subject: $subject");
self::logEmailStatus($recipient, 'sent');
return true;
} catch (Exception $e) {
$retryCount++;
$error = $mail->ErrorInfo;
Logger::logWarning("Email send failed for $recipient (Attempt $retryCount/$retryLimit): $error");
if (str_contains($error, '452 4.3.1')) {
Logger::logWarning("Transient error detected for $recipient: $error");
} elseif (str_contains($error, '550')) {
Logger::logError("Permanent error detected for $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
} elseif (str_contains($error, '421')) {
Logger::logWarning("Rate-limiting error detected for $recipient: $error");
} else {
Logger::logError("Unhandled SMTP error for $recipient: $error");
}
if (str_contains($error, '452') || str_contains($error, '421')) {
if ($retryCount > $retryLimit) {
Logger::logError("Exceeded retry limit for email to $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
}
sleep(5);
continue;
}
Logger::logError("Email permanently failed for $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
}
}
return false;
}
/**
* Process the email queue and send emails in batches.
*
* @param int $batchSize Number of emails to process in a single batch.
* @param int $maxRetries Maximum retry attempts for failed emails.
* @return void
*/
public static function processEmailQueue(int $batchSize = 10, int $maxRetries = 3): void
{
for ($i = 0; $i < $batchSize; $i++) {
$emailData = self::$queueUtility->dequeue('email');
if ($emailData === null) {
Logger::logInfo("No more emails to process in the queue.");
break;
}
$success = self::sendEmail(
$emailData['recipient'],
$emailData['subject'],
$emailData['templatePath'],
$emailData['templateData'],
$emailData['options']
);
if (!$success) {
$retries = $emailData['retries'] ?? 0;
if ($retries < $maxRetries) {
$priority = $emailData['priority'] ?? 0;
$emailData['retries'] = $retries + 1;
self::$queueUtility->enqueue('email', $emailData, $priority);
Logger::logWarning(
"Email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
);
} else {
Logger::logError("Email permanently failed for recipient: {$emailData['recipient']}");
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
}
} else {
self::logEmailStatus($emailData['recipient'], 'sent');
}
}
Logger::logInfo("Email queue processing completed.");
}
/**
* Process contact-related email queue.
*
* @param int $batchSize Number of emails to process in a single batch.
* @param int $maxRetries Maximum retry attempts for failed emails.
* @return void
*/
public static function processContactQueue(int $batchSize = 10, int $maxRetries = 3): void
{
Logger::logInfo("Processing contact email queue...");
for ($i = 0; $i < $batchSize; $i++) {
$emailData = self::$queueUtility->dequeue('contact_email');
if ($emailData === null) {
Logger::logInfo("No more emails to process in the contact queue.");
break;
}
$success = self::sendEmail(
$emailData['recipient'],
$emailData['subject'],
$emailData['templatePath'],
$emailData['templateData'],
$emailData['options']
);
if (!$success) {
$retries = $emailData['retries'] ?? 0;
if ($retries < $maxRetries) {
$priority = $emailData['priority'] ?? 0;
$emailData['retries'] = $retries + 1;
self::$queueUtility->enqueue('contact_email', $emailData, $priority);
Logger::logWarning(
"Contact email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
);
} else {
Logger::logError("Contact email permanently failed for recipient: {$emailData['recipient']}");
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
}
} else {
self::logEmailStatus($emailData['recipient'], 'sent');
}
}
Logger::logInfo("Contact email queue processing completed.");
}
/**
* Retrieve the status of a specific email by recipient.
*
* @param string $recipient Email address of the recipient.
* @return array|null The email status or null if not found.
*/
public static function getEmailStatus(string $recipient): ?array
{
try {
$db = Database::getInstance();
$query = "SELECT * FROM email_status WHERE recipient = :recipient ORDER BY created_at DESC LIMIT 1";
$params = [':recipient' => $recipient];
return $db->fetchOne($query, $params);
} catch (\Throwable $e) {
Logger::logError("Failed to retrieve email status for $recipient: " . $e->getMessage());
return null;
}
}
/**
* Clear the email queue.
*
* @param string $queueName The name of the queue to clear (default: 'email').
* @return void
*/
public static function clearQueue(string $queueName = 'email'): void
{
Logger::logInfo("Clearing queue: $queueName");
try {
self::$queueUtility->clearQueue($queueName);
Logger::logInfo("Queue $queueName cleared successfully.");
} catch (\Throwable $e) {
Logger::logError("Failed to clear queue $queueName: " . $e->getMessage());
}
}
/**
* List all queued emails in a specific queue.
*
* @param string $queueName The name of the queue to inspect (default: 'email').
* @return array List of queued emails.
*/
public static function listQueuedEmails(string $queueName = 'email'): array
{
try {
Logger::logInfo("Listing emails in queue: $queueName");
return self::$queueUtility->listQueue($queueName);
} catch (\Throwable $e) {
Logger::logError("Failed to list emails in queue $queueName: " . $e->getMessage());
return [];
}
}
}

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

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

View File

@ -0,0 +1,86 @@
<?php
/**
* ============================================
* File: Sanitizer.php
* Path: /app/Utils/
* Purpose: Utility class for sanitizing all user input securely
* Version: 1.1
* Author: Wizdom Networks (merged from HelpDesk+ and WizdomWeb)
* Usage: Called wherever input data needs cleaning before use or DB insertion
* ============================================
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class Sanitizer
{
/**
* Basic string sanitization (removes HTML and encodes entities)
*/
public static function sanitizeString(string $value): string
{
try {
$sanitized = htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8');
Logger::debug("Sanitized string: Original: $value | Sanitized: $sanitized");
return $sanitized;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return '';
}
}
/**
* Deep sanitize string using multiple layers (chained method)
*/
public static function sanitizeChained(string $value): string
{
return htmlspecialchars(strip_tags(trim(filter_var($value, FILTER_SANITIZE_STRING))), ENT_QUOTES, 'UTF-8');
}
/**
* Sanitize input with tag stripping and encoding
*/
public static function sanitizeInput(string $value): string
{
return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8');
}
/**
* Sanitize email
*/
public static function sanitizeEmail(string $value): string
{
$sanitized = filter_var(trim($value), FILTER_SANITIZE_EMAIL);
Logger::debug("Sanitized email: Original: $value | Sanitized: $sanitized");
return $sanitized;
}
/**
* Sanitize URL
*/
public static function sanitizeURL(string $value): string
{
$sanitized = filter_var(trim($value), FILTER_SANITIZE_URL);
Logger::debug("Sanitized URL: Original: $value | Sanitized: $sanitized");
return $sanitized;
}
/**
* Recursively sanitize array
*/
public static function sanitizeArray(array $data): array
{
$clean = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$clean[$key] = self::sanitizeArray($value);
} else {
$clean[$key] = self::sanitizeString((string) $value);
}
}
return $clean;
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* ============================================
* File: SubmissionCheck.php
* Path: /app/Utilities/
* Purpose: Utility to check for recent form submissions by email
* Namespace: WizdomNetworks\WizeWeb\Utilities
* Version: 1.0
* Author: Wizdom Networks
* ============================================
*/
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utilities\Database;
use PDO;
use Exception;
class SubmissionCheck
{
/**
* Check if the given email has a recent submission in the specified table.
*
* @param string $email - The email address to check.
* @param string $table - The name of the table (e.g., contact_messages, subscribers).
* @param string $emailField - The name of the email field in the table.
* @param string $timestampField - The name of the datetime/timestamp field.
* @param int $days - The number of days to consider as "recent" (default: 7).
* @return array|null - Returns array with date of last submission if found, or null if none.
* @throws Exception - If query fails.
*/
public static function hasRecentSubmission(string $email, string $table, string $emailField, string $timestampField, int $days = 7): ?array
{
$pdo = Database::getConnection();
$sql = "SELECT $timestampField FROM $table WHERE $emailField = :email ORDER BY $timestampField DESC LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute(['email' => $email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && isset($row[$timestampField])) {
$last = new \DateTime($row[$timestampField]);
$cutoff = (new \DateTime())->modify("-{$days} days");
if ($last >= $cutoff) {
return ['submitted_at' => $last->format('Y-m-d H:i:s')];
}
}
return null;
}
}

238
app/Utilities/Validator.php Normal file
View File

@ -0,0 +1,238 @@
<?php
namespace WizdomNetworks\WizeWeb\Utilities;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
/**
* Validator Utility
*
* Provides methods for validating user input.
*/
class Validator
{
/**
* Check if a value is non-empty.
*
* @param string $value The value to check.
* @return bool True if the value is not empty, false otherwise.
*/
public static function isRequired(string $value): bool
{
try {
Logger::info("[DEBUG] Checking if value is required: $value");
$isValid = !empty(trim($value));
if (!$isValid) {
Logger::warning("[WARNING] Value is required but empty.");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Validate an email address.
*
* @param string $email The email address to validate.
* @return bool True if the email is valid, false otherwise.
*/
public static function isEmail(string $email): bool
{
try {
Logger::info("[DEBUG] Validating email address: $email");
$isValid = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
if (!$isValid) {
Logger::warning("[WARNING] Invalid email address: $email");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Validate a URL.
*
* @param string $url The URL to validate.
* @return bool True if the URL is valid, false otherwise.
*/
public static function isURL(string $url): bool
{
try {
Logger::info("[DEBUG] Validating URL: $url");
$isValid = filter_var($url, FILTER_VALIDATE_URL) !== false;
if (!$isValid) {
Logger::warning("[WARNING] Invalid URL: $url");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Check if a string matches a given regular expression.
*
* @param string $string The string to validate.
* @param string $pattern The regular expression to match.
* @return bool True if the string matches the pattern, false otherwise.
*/
public static function matchesRegex(string $string, string $pattern): bool
{
try {
Logger::info("[DEBUG] Validating string against regex: Pattern=$pattern");
$isValid = preg_match($pattern, $string) === 1;
if (!$isValid) {
Logger::warning("[WARNING] String does not match regex: $string");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Check if a string has a minimum length.
*
* @param string $string The string to check.
* @param int $minLength The minimum length.
* @return bool True if the string meets the minimum length, false otherwise.
*/
public static function hasMinLength(string $string, int $minLength): bool
{
try {
Logger::info("[DEBUG] Checking if string has minimum length: $minLength");
$isValid = strlen(trim($string)) >= $minLength;
if (!$isValid) {
Logger::warning("[WARNING] String is shorter than minimum length: $string");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Check if a string has a maximum length.
*
* @param string $string The string to check.
* @param int $maxLength The maximum length.
* @return bool True if the string meets the maximum length, false otherwise.
*/
public static function hasMaxLength(string $string, int $maxLength): bool
{
try {
Logger::info("[DEBUG] Checking if string has maximum length: $maxLength");
$isValid = strlen(trim($string)) <= $maxLength;
if (!$isValid) {
Logger::warning("[WARNING] String exceeds maximum length: $string");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Validate a string length.
*
* @param string $input The string to validate.
* @param int $min Minimum length.
* @param int $max Maximum length.
* @return bool True if the string length is valid, false otherwise.
*/
public static function validateStringLength(string $input, int $min, int $max): bool
{
try {
Logger::info("[DEBUG] Validating string length: Input='$input', Min=$min, Max=$max");
$length = strlen($input);
$isValid = $length >= $min && $length <= $max;
if (!$isValid) {
Logger::warning("[WARNING] Invalid string length: $length (Expected between $min and $max)");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Validate a boolean value.
*
* @param mixed $input The input to validate as a boolean.
* @return bool True if the input is a valid boolean, false otherwise.
*/
public static function validateBoolean($input): bool
{
try {
Logger::info("[DEBUG] Validating boolean input: $input");
$isValid = is_bool(filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
if (!$isValid) {
Logger::warning("[WARNING] Invalid boolean input: $input");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
/**
* Validate a date format.
*
* @param string $date The date string to validate.
* @param string $format The expected date format (e.g., 'Y-m-d').
* @return bool True if the date matches the format, false otherwise.
*/
public static function validateDate(string $date, string $format = 'Y-m-d'): bool
{
try {
Logger::info("[DEBUG] Validating date: $date with format: $format");
$dateTime = \DateTime::createFromFormat($format, $date);
$isValid = $dateTime && $dateTime->format($format) === $date;
if (!$isValid) {
Logger::warning("[WARNING] Invalid date: $date (Expected format: $format)");
}
return $isValid;
} catch (\Throwable $e) {
ErrorHandler::exception($e);
return false;
}
}
}

View File

@ -1,4 +0,0 @@
<?php
// Placeholder for email template

View File

@ -0,0 +1,6 @@
<p>New Contact Form Submission:</p>
<ul>
<li><strong>Name:</strong> {{name}}</li>
<li><strong>Email:</strong> {{email}}</li>
<li><strong>Message:</strong> {{message}}</li>
</ul>