Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
278 lines
7.7 KiB
PHP
278 lines
7.7 KiB
PHP
<?php
|
|
/**
|
|
* Shared utility functions for the MSP Quote Wizard API.
|
|
*
|
|
* Provides UUID generation, token generation, JSON response helpers,
|
|
* input validation, CORS headers, and logging.
|
|
*/
|
|
|
|
// Deny direct access
|
|
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
|
|
http_response_code(403);
|
|
exit('Direct access denied.');
|
|
}
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
// --------------------------------------------------------------------------
|
|
// UUID / Token generation
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Generate a UUID v4 string (lowercase, 36 chars with hyphens).
|
|
*
|
|
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
* where y is one of 8, 9, a, b.
|
|
*
|
|
* @return string
|
|
*/
|
|
function generate_uuid(): string
|
|
{
|
|
$bytes = random_bytes(16);
|
|
|
|
// Set version to 4 (0100 in binary)
|
|
$bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
|
|
// Set variant to RFC 4122 (10xx in binary)
|
|
$bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
|
|
|
|
return sprintf(
|
|
'%s-%s-%s-%s-%s',
|
|
bin2hex(substr($bytes, 0, 4)),
|
|
bin2hex(substr($bytes, 4, 2)),
|
|
bin2hex(substr($bytes, 6, 2)),
|
|
bin2hex(substr($bytes, 8, 2)),
|
|
bin2hex(substr($bytes, 10, 6))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate a URL-safe access token matching Python's secrets.token_urlsafe(32).
|
|
*
|
|
* Produces a 43-character base64url-encoded string (no padding) from 32
|
|
* random bytes, exactly matching the Python implementation.
|
|
*
|
|
* @return string 43-character URL-safe token
|
|
*/
|
|
function generate_access_token(): string
|
|
{
|
|
$bytes = random_bytes(32);
|
|
// base64url encode: replace +/ with -_, strip padding =
|
|
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// JSON response helpers
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Send a JSON response with the given data and HTTP status code.
|
|
*
|
|
* Sets Content-Type header, outputs JSON, and terminates the script.
|
|
*
|
|
* @param mixed $data Data to encode as JSON.
|
|
* @param int $status HTTP status code (default 200).
|
|
* @return never
|
|
*/
|
|
function json_response($data, int $status = 200): void
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Send a JSON error response.
|
|
*
|
|
* @param string $message Error message.
|
|
* @param int $status HTTP status code (default 400).
|
|
* @param mixed|null $details Additional error details.
|
|
* @return never
|
|
*/
|
|
function error_response(string $message, int $status = 400, $details = null): void
|
|
{
|
|
$body = ['detail' => $message];
|
|
if ($details !== null) {
|
|
$body['errors'] = $details;
|
|
}
|
|
json_response($body, $status);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Request parsing
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Parse the JSON request body.
|
|
*
|
|
* @return array Decoded JSON as an associative array.
|
|
*/
|
|
function get_json_body(): array
|
|
{
|
|
$raw = file_get_contents('php://input');
|
|
if (empty($raw)) {
|
|
return [];
|
|
}
|
|
|
|
$data = json_decode($raw, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
error_response('Invalid JSON in request body', 400);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get the client IP address, accounting for reverse proxies.
|
|
*
|
|
* Checks X-Forwarded-For first, then X-Real-IP, then REMOTE_ADDR.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
function get_client_ip(): ?string
|
|
{
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
return trim($parts[0]);
|
|
}
|
|
|
|
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
|
return trim($_SERVER['HTTP_X_REAL_IP']);
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get the User-Agent header value.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
function get_user_agent(): ?string
|
|
{
|
|
return $_SERVER['HTTP_USER_AGENT'] ?? null;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// CORS
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Emit CORS headers based on the configured allowed origins.
|
|
*
|
|
* For preflight (OPTIONS) requests, this also sets the allowed methods
|
|
* and headers, then terminates the script with 204.
|
|
*/
|
|
function cors_headers(): void
|
|
{
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
$allowed = array_map('trim', explode(',', CORS_ALLOWED_ORIGINS));
|
|
|
|
// Allow the origin if it matches our whitelist, or allow all if '*'
|
|
if (in_array('*', $allowed, true) || in_array($origin, $allowed, true)) {
|
|
$send_origin = in_array('*', $allowed, true) ? '*' : $origin;
|
|
header("Access-Control-Allow-Origin: {$send_origin}");
|
|
}
|
|
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
|
header('Access-Control-Max-Age: 86400');
|
|
|
|
// Handle preflight
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
http_response_code(204);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Validation
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Validate that all required fields are present and non-empty in the data.
|
|
*
|
|
* @param array $data Associative array of input data.
|
|
* @param string[] $fields List of required field names.
|
|
* @return string[] Array of error messages (empty if valid).
|
|
*/
|
|
function validate_required(array $data, array $fields): array
|
|
{
|
|
$errors = [];
|
|
foreach ($fields as $field) {
|
|
if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) {
|
|
$errors[] = "Field '{$field}' is required.";
|
|
}
|
|
}
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Validate an email address.
|
|
*
|
|
* @param string $email Email address to validate.
|
|
* @return bool True if valid.
|
|
*/
|
|
function validate_email(string $email): bool
|
|
{
|
|
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Logging
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Append a message to the application log file.
|
|
*
|
|
* @param string $level Log level (INFO, WARNING, ERROR).
|
|
* @param string $message Log message.
|
|
*/
|
|
function app_log(string $level, string $message): void
|
|
{
|
|
$dir = dirname(LOG_FILE);
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0750, true);
|
|
}
|
|
|
|
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
|
|
$line = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
|
|
@file_put_contents(LOG_FILE, $line, FILE_APPEND | LOCK_EX);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Datetime helpers
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a datetime value for JSON output (ISO 8601 format).
|
|
*
|
|
* Accepts a datetime string from MySQL (Y-m-d H:i:s) and returns
|
|
* an ISO 8601 string, or null if input is null/empty.
|
|
*
|
|
* @param string|null $dt MySQL datetime string.
|
|
* @return string|null ISO 8601 formatted string.
|
|
*/
|
|
function format_datetime(?string $dt): ?string
|
|
{
|
|
if ($dt === null || $dt === '' || $dt === '0000-00-00 00:00:00') {
|
|
return null;
|
|
}
|
|
// MySQL DATETIME is already in UTC for this application
|
|
$ts = strtotime($dt);
|
|
if ($ts === false) {
|
|
return null;
|
|
}
|
|
return gmdate('Y-m-d\TH:i:s\Z', $ts);
|
|
}
|
|
|
|
/**
|
|
* Get the current UTC datetime in MySQL format.
|
|
*
|
|
* @return string Y-m-d H:i:s
|
|
*/
|
|
function utc_now(): string
|
|
{
|
|
return gmdate('Y-m-d H:i:s');
|
|
}
|