Files
votervue/classes/Process_ExportRequest.php
T

784 lines
24 KiB
PHP
Raw Normal View History

2026-07-03 15:46:56 -04:00
<?php
declare(strict_types=1);
$basePath = realpath(__DIR__ . '/..');
require_once $basePath . '/vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
// =====================
// Process_ExportRequest
// =====================
class Process_ExportRequest {
// Variables
private $log = "";
private $settings = "";
private $connection = "";
// ==================
// Class Constructor.
// ==================
public function __construct() {
$basePath = realpath(__DIR__ . '/..');
$logfile = $basePath . "/scripts/exportrequests.log";
// ==========================
// Log Rotation (50 MB limit)
// ==========================
$maxLogSize = 50 * 1024 * 1024; // 50 MB
if (file_exists($logfile) && filesize($logfile) >= $maxLogSize) {
$rotated = $logfile . '-';
rename($logfile, $rotated);
}
$this->log = fopen($logfile, "a");
if ($this->log === false) {
error_log("Process_ExportRequest : Unable to open log file");
exit();
}
$this->log(str_repeat("*", 50));
// Get Settings
$this->settings = new SimpleXMLElement((file_exists($basePath . "/xml/settings.xml") ? file_get_contents($basePath . "/xml/settings.xml") : "<settings/>"));
if ($this->settings->count() === 0) {
error_log("Process_ExportRequest : Unable to initialize settings");
exit();
}
// Open Database Connections
$this->connection = $this->connect();
}
// ==============================================
// Return a VoterVue Database Database Connection
// ==============================================
public function connect() {
if ($this->settings->Environment == 'Production') {
$host = 'localhost';
} else {
$host = '192.168.1.190';
}
try {
$database = (string) $this->settings->DatabaseName;
$user = (string) $this->settings->DatabaseUser;
$password = (string) $this->settings->DatabasePassword;
$connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);
$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException | Exception $e) {
trigger_error($e->getMessage());
}
return $connection;
}
// =============================
// Log a message to the Log File
// =============================
private function log($message = "") {
fwrite($this->log, date("Y-m-d H:i A") . " : " . "{$message} \n");
}
// ===================
// Process Parcel Data
// ===================
public function process() {
$this->log("Starting Searched Record Export Job");
$this->runExportJob();
$this->purgeExportJobs();
$this->log("Completed Searched Record Export Job");
}
// ==============
// Run Export Job
// ==============
private function runExportJob() {
$this->log("Processing Export Job");
$nextjob = $this->claimNextJob();
if (!$nextjob) {
$this->log("Nothing to do.");
return;
}
$exportjob_serial = (int) $nextjob['exportjob_serial'];
$exportjob_license_type = (string) $nextjob['exportjob_license_type'];
$exportjob_searchcriteria = (string) $nextjob['exportjob_searchcriteria'];
$exportjob_file_name = (string) $nextjob['exportjob_file_name'];
$exportjob_file_type = (string) $nextjob['exportjob_file_type'];
try {
switch ($exportjob_license_type) {
case "recreational":
$recreational_licenses = $this->getRecreationalLicenses($exportjob_searchcriteria);
$export_file = $this->createExportFile($exportjob_file_name, $exportjob_file_type, $recreational_licenses, $exportjob_license_type);
break;
case "commercial":
// $comm_licenses = $this->getPZFactsFullDataset($user_subscription_start_date, $user_subscription_end_date, $counties);
// $result = $this->generatePZFactsExportFile($exportjob_file_name, $exportjob_file_type, $pzfacts, $subscription);
break;
}
$exportjob_download_token = hash('sha256', random_bytes(32));
$SQL = "UPDATE exportjobs
SET exportjob_status = 'complete',
exportjob_completed = NOW(),
exportjob_file_path = :exportjob_file_path,
exportjob_file_type = :exportjob_file_type,
exportjob_download_token = :exportjob_download_token
WHERE exportjob_serial = :exportjob_serial ";
$update = $this->connect()->prepare($SQL);
$update->execute([':exportjob_file_path' => $export_file['exportjob_file_path'], ":exportjob_download_token" => $exportjob_download_token,
':exportjob_serial' => $exportjob_serial, ':exportjob_file_type' => $export_file['exportjob_file_type']]);
} catch (Throwable $e) {
$SQL = "UPDATE exportjobs
SET exportjob_status = 'failed',
exportjob_completed = NOW(),
exportjob_error_message = :exportjob_error_message
WHERE exportjob_serial = :exportjob_serial";
$fail = $this->connect()->prepare($SQL);
$fail->execute([':exportjob_serial' => $exportjob_serial, ':exportjob_error_message' => $e->getMessage()]);
// Notify Admins of error
$this->sendWebHookAlert("Export Job {$exportjob_serial} has failed. Check Error Logs for details.");
$this->email_admins("Export Job Failure.", "Export Job {$exportjob_serial} has failed. Check Error Logs for details.");
}
}
// =================================
// Purge Exports Older then 24 Hours
// =================================
private function purgeExportJobs() {
$this->log("Purging Export Jobs Database Files");
$this->purgeFiles();
}
// =======================
// Claim Next Job in Queue
// =======================
private function claimNextJob() {
$this->connection->beginTransaction();
$SQL = "SELECT view_exportjobs.*
FROM view_exportjobs
WHERE view_exportjobs.exportjob_status = 'queued'
ORDER BY view_exportjobs.exportjob_created ASC
LIMIT 1 FOR UPDATE";
$statement = $this->connection->prepare($SQL);
$statement->execute();
$job = $statement->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$this->connection->commit();
return null;
}
$SQL = "UPDATE exportjobs
SET exportjob_status = 'running'
WHERE exportjob_serial = :exportjob_serial";
$statement = $this->connection->prepare($SQL);
$statement->execute([':exportjob_serial' => (int) $job['exportjob_serial']]);
$this->connection->commit();
return $job;
}
// ==================================
// Return a Users Subscription Object
// ==================================
private function getUserSubscription($subscription, $user) {
$SQL = "select view_usersubscriptions.*
from view_usersubscriptions
where view_usersubscriptions.usersubscription_user = {$user}
and view_usersubscriptions.usersubscription_subscription = {$subscription}";
$usersubscription = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
return $usersubscription;
}
// ============================
// Return a Subscription Object
// ============================
private function getSubscription($subscription) {
$SQL = "select view_subscriptions.*
from view_subscriptions
where subscription_serial = {$subscription}";
$subscription = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
return $subscription;
}
// ================================
// Get Recreational License Dataset
// ================================
private function getRecreationalLicenses($search_criteria) {
set_time_limit(0);
$criteria = json_decode($search_criteria, true);
if (!is_array($criteria)) {
return [];
}
$connection = $this->connect();
// -------------------------------------------------
// Convert county serials to county text values
// -------------------------------------------------
if (!empty($criteria['license_county'])) {
$countySerials = array_filter(array_map('trim', explode(',', $criteria['license_county'])));
if (!empty($countySerials)) {
$placeholders = [];
foreach ($countySerials as $index => $serial) {
$placeholders[] = ":county{$index}";
}
$SQL = "select dropdown_value
from dropdowns
where dropdown_serial in (" . implode(',', $placeholders) . ")";
$statement = $connection->prepare($SQL);
foreach ($countySerials as $index => $serial) {
$statement->bindValue(":county{$index}", $serial, PDO::PARAM_INT);
}
$statement->execute();
$criteria['license_county'] = $statement->fetchAll(PDO::FETCH_COLUMN);
}
}
// --------------------------------------------------
// Convert ethnicity serials to ethnicity text values
// --------------------------------------------------
if (!empty($criteria['license_ethnicity'])) {
$ethnicitySerials = array_filter(array_map('trim', explode(',', $criteria['license_ethnicity'])));
if (!empty($ethnicitySerials)) {
$placeholders = [];
foreach ($ethnicitySerials as $index => $serial) {
$placeholders[] = ":ethnicity{$index}";
}
$SQL = "select dropdown_value
from dropdowns
where dropdown_serial in (" . implode(',', $placeholders) . ")";
$statement = $connection->prepare($SQL);
foreach ($ethnicitySerials as $index => $serial) {
$statement->bindValue(":ethnicity{$index}", $serial, PDO::PARAM_INT);
}
$statement->execute();
$criteria['license_ethnicity'] = $statement->fetchAll(PDO::FETCH_COLUMN);
}
}
// --------------------------------
// Build recreational license query
// --------------------------------
$allowedColumns = [
'license_last_name',
'license_first_name',
'license_county',
'license_city',
'license_type',
'license_gender',
'license_ethnicity',
'license_start_date',
'license_expire_date'
];
$where = [];
$params = [];
foreach ($criteria as $key => $value) {
if (!in_array($key, $allowedColumns, true)) {
continue;
}
if (is_array($value)) {
if (empty($value)) {
continue;
}
$placeholders = [];
foreach ($value as $index => $item) {
$param = ":" . $key . "_" . $index;
$placeholders[] = $param;
$params[$param] = $item;
}
$where[] = "{$key} in (" . implode(',', $placeholders) . ")";
} else {
$value = trim($value);
if ($value === '') {
continue;
}
if (strpos($value, ',') !== false) {
$items = array_filter(array_map('trim', explode(',', $value)));
$placeholders = [];
foreach ($items as $index => $item) {
$param = ":" . $key . "_" . $index;
$placeholders[] = $param;
$params[$param] = $item;
}
$where[] = "{$key} in (" . implode(',', $placeholders) . ")";
} elseif ($key === 'license_start_date') {
$where[] = "{$key} >= :{$key}";
$params[":{$key}"] = date('Y-m-d', strtotime($value));
} elseif ($key === 'license_expire_date') {
$where[] = "{$key} <= :{$key}";
$params[":{$key}"] = date('Y-m-d', strtotime($value));
} else {
$where[] = "{$key} like :{$key}";
$params[":{$key}"] = '%' . $value . '%';
}
}
}
$SQL = "select fwc_recreational_licenses.*
from fwc_recreational_licenses";
if (!empty($where)) {
$SQL .= " where " . implode(" and ", $where);
}
$SQL .= " order by license_last_name, license_first_name";
$statement = $connection->prepare($SQL);
foreach ($params as $param => $value) {
$statement->bindValue($param, $value);
}
$statement->execute();
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
// ======================================
// Get Planning/Zoning Facts Full Dataset
// ======================================
private function getPZFactsFullDataset($user_subscription_start_date, $user_subscription_end_date, $counties) {
set_time_limit(0);
// determine real date range based upon users subscription start date
if ($user_subscription_start_date <= date('Y-m-d', strtotime('-1 year'))) {
$user_subscription_start_date = date('Y-m-d', strtotime('-1 year'));
}
$SQL = "select view_pzfacts.*
from view_pzfacts
where view_pzfacts.pzfact_entry_date between '{$user_subscription_start_date}' and '{$user_subscription_end_date}'
and view_pzfacts.pzfact_county in ($counties)";
$pzfacts = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
return $pzfacts;
}
// ===============================
// Get Business Facts Full Dataset
// ===============================
private function getBusinessFactsFullDataset($user_subscription_start_date, $user_subscription_end_date, $counties) {
set_time_limit(0);
// determine real date range based upon users subscription start date
if ($user_subscription_start_date <= date('Y-m-d', strtotime('-1 year'))) {
$user_subscription_start_date = date('Y-m-d', strtotime('-1 year'));
}
$SQL = "select view_businessfacts.*
from view_businessfacts
where view_businessfacts.businessfact_entry_date between '{$user_subscription_start_date}' and '{$user_subscription_end_date}'
and view_businessfacts.businessfact_county in ($counties)";
$businessfacts = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
return $businessfacts;
}
// ========================
// Return a Counties Object
// ========================
private function getSubscriptionCounties($subscription_serial) {
$SQL = "select view_subscriptioncounties.*
from view_subscriptioncounties
where view_subscriptioncounties.subscriptioncounty_subscription = '{$subscription_serial}'";
$counties = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
$countyNames = [];
foreach ($counties as $county) {
$countyNames[] = $county['subscriptioncounty_county'];
}
$counties = implode(', ', $countyNames);
return $counties;
}
// ====================
// Purges Files on Disk
// ====================
private function purgeFiles() {
$SQL = "select *
from exportjobs
where exportjobs.exportjob_created < (NOW() - INTERVAL 24 HOUR)";
$exportfiles = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
if (count($exportfiles) == 0) {
$this->log("Nothing To Purge");
return;
}
foreach ($exportfiles as $files) {
$filename = $files['exportjob_file_path'];
unlink($filename);
}
$baseDir = './exports';
foreach (new DirectoryIterator($baseDir) as $item) {
if ($item->isDot() || !$item->isDir()) {
continue;
}
$folderPath = $item->getPathname();
// Check if directory is empty
$files = scandir($folderPath);
if ($files !== false && count($files) === 2) {
rmdir($folderPath);
}
}
$this->log("Purging Export Jobs Database Records");
$SQL = "delete from exportjobs
where exportjobs.exportjob_created < (NOW() - INTERVAL 24 HOUR)";
$this->connection->query($SQL);
}
// ==================
// Create Export File
// ==================
private function createExportFile($exportjob_file_name, $exportjob_file_type, $recreational_licenses, $exportjob_license_type) {
$job_code = $this->getUniqueJobCode();
//Create a dedicated job folder
$base_directory = "./exports/{$job_code}";
if (!is_dir($base_directory)) {
mkdir($base_directory, 0777, true);
}
$exportjob_file_path = "{$base_directory}/{$exportjob_file_name}";
switch ($exportjob_file_type) {
case "CSV":
// Create a dedicated job folder
$base_directory = "./exports/{$job_code}";
if (!is_dir($base_directory)) {
mkdir($base_directory, 0777, true);
}
$exportjob_file_path = "{$base_directory}/{$exportjob_file_name}";
$file_pointer = (fopen($exportjob_file_path, 'w'));
if (!$file_pointer) {
throw new RuntimeException("Unable to write: {$exportjob_file_path}");
}
// Add CSV headers
fputcsv($file_pointer, ['lastname', 'firstname', 'Gender', 'ethnicity', 'county', 'city', 'licence_type', 'start_date', 'expire_date'], ',', '"', '\\');
foreach ($recreational_licenses as $recreational_license) {
fputcsv($file_pointer, $recreational_license, ',', '"', '\\');
}
fclose($file_pointer);
break;
case "PDF":
$this->createPDFExport_ResidentialLicenses($recreational_licenses, $base_directory, $exportjob_file_name);
break;
}
$exportjob_file_path = str_replace('../exports/', './exports/', $exportjob_file_path);
$exportjob_file_type = strtoupper(pathinfo($exportjob_file_path, PATHINFO_EXTENSION));
return ['exportjob_file_name' => $exportjob_file_name, 'exportjob_file_path' => $exportjob_file_path, 'exportjob_file_type' => $exportjob_file_type];
}
// =======================
// Format Raw Phone Number
// =======================
public function formatPhoneNumber(string $phonenumber): string {
// Remove everything except digits
$digits = preg_replace('/\D+/', '', $phonenumber);
// Handle leading country code (US)
if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
$digits = substr($digits, 1);
}
return substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4);
}
// ========================
// Creates Permit Facts PDF
// ========================
private function createPDFExport_ResidentialLicenses($permitfacts, $exportjob_base_directory, $exportjob_file_name) {
require_once ("./reports/Print_SearchedLicensesPDF.php");
$report = (new Print_SearchedLicensesPDF())->print($permitfacts, $exportjob_base_directory, $exportjob_file_name);
return $report;
}
// ======================
// Return Unique Job Code
// ======================
private function getUniqueJobCode() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff),
random_int(0, 0x0fff) | 0x4000, random_int(0, 0x3fff) | 0x8000,
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
}
// ==========================
// Send an Alert/Notification
// ==========================
public function sendWebHookAlert($message, $title = "🚨 DEC Application Alert", $alerttype = "1fff00") {
$webhookUrl = (string) $this->settings->DiscordWebHookURL;
$data = [
'embeds' => [[
'title' => $title,
'description' => $message,
'color' => hexdec($alerttype),
'footer' => [
'text' => 'Real Estate Data Inc. System Notificiation',
],
'timestamp' => date('c'), // ISO 8601 format
]]
];
$jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$response = curl_exec($ch);
if ($response === false) {
$this->log('Curl error: ' . curl_error($ch));
}
curl_close($ch);
}
// ============
// Email Admins
// ============
public function email_admins($subject = "", $message = "") {
$this->log(date("Y-m-d H:i:s") . " - Emailing Admins...");
$settings = new SimpleXMLElement((file_exists("./xml/settings.xml") ? file_get_contents("./xml/settings.xml") : "<settings/>"));
// Server Variables
$host = (string) $settings->SmtpServer;
$username = (string) $settings->SmtpUserName;
$password = (string) $settings->SmtpPassword;
// Recipient Variables
$from_email = (string) $settings->FromEmail;
$from_name = (string) $settings->FromName;
$support_email = (string) $settings->SupportEmail;
$support_name = (string) $settings->SupportName;
if ((string) $settings->Environment == 'Production') {
$admin_email = (string) $settings->AdminEmail;
$admin_name = (string) $settings->AdminName;
}
// Content Variables
$attachment_name = "";
$mail = new PHPMailer(true);
try {
//Server settings
$mail->isSMTP();
$mail->Host = $host;
$mail->SMTPAuth = true;
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = 465;
//Recipients
$mail->setFrom($from_email, $from_name);
$mail->addAddress($support_email, $support_name);
if ($settings->Environment == 'Production') {
$mail->addAddress($admin_email, $admin_name);
}
//Attachments
$mail->addAttachment($attachment_name);
//Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $message;
$mail->send();
} catch (Exception $e) {
$this->log("Message could not be sent. Mailer Error: {$e->ErrorInfo}");
}
}
}
?>