bootstrap.php
13.99 KB
<?php
/**
* WorkersPanel Bootstrap
* Shared runtime helpers
*/
define('BASE_PATH', __DIR__);
require_once __DIR__ . '/config/version.php';
require_once __DIR__ . '/config/app_paths.php';
if (!defined('DEVELOPMENT')) define('DEVELOPMENT', false);
if (!function_exists('wp_instance_key')) {
function wp_instance_key(): string {
static $key = null;
if ($key !== null) return $key;
$host = strtolower((string)($_SERVER['HTTP_HOST'] ?? 'localhost'));
$base = function_exists('app_base_path') ? app_base_path() : '';
$real = realpath(BASE_PATH) ?: BASE_PATH;
$key = substr(hash('sha256', $host . '|' . $base . '|' . $real), 0, 24);
return $key;
}
}
if (!function_exists('wp_cookie_path')) {
function wp_cookie_path(): string {
$base = function_exists('app_base_path') ? app_base_path() : '';
return $base !== '' ? rtrim($base, '/') . '/' : '/';
}
}
if (!function_exists('wp_cookie_secure')) {
function wp_cookie_secure(): bool {
return (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off')
|| (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
}
}
if (!function_exists('wp_app_session_name')) {
function wp_app_session_name(): string {
return 'WPSESS_' . strtoupper(substr(wp_instance_key(), 0, 16));
}
}
if (!function_exists('wp_remember_cookie_name')) {
function wp_remember_cookie_name(): string {
return 'WP_REMEMBER_' . strtoupper(substr(wp_instance_key(), 0, 16));
}
}
if (session_status() === PHP_SESSION_NONE) {
if (!headers_sent()) {
ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
session_name(wp_app_session_name());
session_set_cookie_params([
'lifetime' => 0,
'path' => wp_cookie_path(),
'domain' => '',
'secure' => wp_cookie_secure(),
'httponly' => true,
'samesite' => 'Lax',
]);
}
session_start();
}
if (isset($_SESSION['wp_instance_key']) && $_SESSION['wp_instance_key'] !== wp_instance_key()) {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, [
'path' => $p['path'] ?? wp_cookie_path(),
'domain' => $p['domain'] ?? '',
'secure' => (bool)($p['secure'] ?? wp_cookie_secure()),
'httponly' => (bool)($p['httponly'] ?? true),
'samesite' => $p['samesite'] ?? 'Lax',
]);
}
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy();
}
session_start();
}
if (!isset($_SESSION['wp_instance_key'])) {
$_SESSION['wp_instance_key'] = wp_instance_key();
}
// Install check
if (!file_exists(BASE_PATH . '/install.lock')) {
$uri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($uri, '/install') === false) {
app_redirect('install');
}
} else {
$db_config = BASE_PATH . '/config/database.php';
if (!file_exists($db_config)) die('Database config missing. Delete install.lock to reinstall.');
require_once $db_config;
// Set session variables for DB audit triggers
if (isset($pdo) && $pdo instanceof PDO) {
try {
$uid = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
$pdo->exec("SET @app_user_id = " . ($uid ?? "NULL"));
$pdo->exec("SET @app_ip = " . ($ip ? $pdo->quote($ip) : "NULL"));
} catch (Throwable $e) {}
}
}
// ── DB helpers ─────────────────────────────────────────────
/**
* Returns true if a column exists on a table in the current database.
* (Used to keep updates compatible while schema changes roll out.)
*/
function wp_db_column_exists(PDO $pdo, string $table, string $column): bool {
try {
$stmt = $pdo->prepare(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1"
);
$stmt->execute([$table, $column]);
return (bool)$stmt->fetchColumn();
} catch (Throwable $e) {
return false;
}
}
/**
* Returns the first existing column from a preferred list, or null.
*/
function wp_db_pick_column(PDO $pdo, string $table, array $candidates): ?string {
foreach ($candidates as $col) {
if (wp_db_column_exists($pdo, $table, $col)) return $col;
}
return null;
}
// ── Auth helpers ────────────────────────────────────────────
function isLoggedIn(): bool {
return isset($_SESSION['user_id'], $_SESSION['user_role']);
}
function wp_normalize_role_name(?string $role): string {
$role = strtolower(trim((string)$role));
return str_replace(['_', ' '], '-', $role);
}
function isAdminRole(): bool {
if (!isLoggedIn()) {
return false;
}
$role = wp_normalize_role_name($_SESSION['user_role'] ?? '');
return in_array($role, ['administrator', 'director', 'admin', 'super-admin', 'superadmin'], true);
}
function hasPermission(string $permission): bool {
if (!isLoggedIn()) return false;
$role = wp_normalize_role_name($_SESSION['user_role'] ?? '');
if (in_array($role, ['driver', 'porter'], true) && str_starts_with($permission, 'calendar.')) {
return false;
}
if (isAdminRole()) return true;
return in_array($permission, $_SESSION['permissions'] ?? [], true);
}
function requireAuth(): void {
if (!isLoggedIn()) { app_redirect('login'); }
}
function requireAdmin(): void {
requireAuth();
if (!isAdminRole()) { http_response_code(403); die('Access denied. Admin required.'); }
}
function requirePermission(string $permission): void {
requireAuth();
if (!hasPermission($permission)) { http_response_code(403); die('Access denied.'); }
}
function hasAnyPermission(array $permissions): bool {
if (!isLoggedIn()) return false;
if (isAdminRole()) return true;
foreach ($permissions as $permission) {
if (hasPermission((string)$permission)) return true;
}
return false;
}
function requireAnyPermission(array $permissions): void {
requireAuth();
if (!hasAnyPermission($permissions)) { http_response_code(403); die('Access denied.'); }
}
function wp_db_table_exists(string $table): bool {
global $pdo;
if (!isset($pdo) || !($pdo instanceof PDO)) return false;
try {
$stmt = $pdo->prepare("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1");
$stmt->execute([$table]);
return (bool)$stmt->fetchColumn();
} catch (Throwable $e) {
return false;
}
}
function wp_clock_status(?int $userId = null): array {
global $pdo;
$userId = $userId ?? (isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0);
if ($userId <= 0 || !isset($pdo) || !($pdo instanceof PDO) || !wp_db_table_exists('time_clock_entries')) {
return ['status' => 'not_required'];
}
try {
$stmt = $pdo->prepare("SELECT * FROM time_clock_entries WHERE user_id = ? AND DATE(clock_in_at) = CURDATE() ORDER BY clock_in_at DESC LIMIT 1");
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) return ['status' => 'not_clocked_in'];
if (!empty($row['clock_out_at']) || strtolower((string)($row['status'] ?? '')) === 'clocked_out') {
return ['status' => 'shift_ended', 'entry' => $row];
}
return ['status' => 'clocked_in', 'entry' => $row];
} catch (Throwable $e) {
return ['status' => 'not_clocked_in'];
}
}
function wp_clock_required_for_current_user(): bool {
return isLoggedIn() && !in_array(wp_normalize_role_name($_SESSION['user_role'] ?? ''), ['administrator', 'director', 'admin', 'super-admin', 'superadmin'], true);
}
function wp_clock_available_for_current_user(): bool {
return isLoggedIn() && wp_db_table_exists('time_clock_entries');
}
function wp_clock_allowed_route(string $route): bool {
return in_array($route, ['/clock', '/logout', '/password-change'], true);
}
function wp_enforce_clock_gate(): void {
if (!wp_clock_required_for_current_user()) return;
$route = app_request_path();
if (wp_clock_allowed_route($route)) return;
$status = wp_clock_status();
if (($status['status'] ?? '') !== 'clocked_in') {
app_redirect('clock');
}
}
function getCurrentUser(): ?array {
if (!isLoggedIn()) return null;
global $pdo;
try {
$stmt = $pdo->prepare("SELECT id, username, email, display_name, role, secondary_group FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch() ?: null;
} catch (Throwable $e) {
return null;
}
}
// ── Activity logging ─────────────────────────────────────────
// Supports both old signature: logActivity($action, $description)
// And new signature: logActivity($action, $entity_type, $entity_id, $description)
function logActivity(string $action, $secondArg = null, $thirdArg = null, $fourthArg = null): void {
if (!isLoggedIn()) return;
global $pdo;
// Detect call signature
if ($fourthArg !== null) {
// New: logActivity($action, $entity_type, $entity_id, $description)
$entity_type = $secondArg;
$entity_id = $thirdArg;
$description = $fourthArg;
} elseif ($thirdArg !== null && is_int($thirdArg)) {
// New: logActivity($action, $entity_type, $entity_id)
$entity_type = $secondArg;
$entity_id = $thirdArg;
$description = null;
} else {
// Old: logActivity($action, $description)
$entity_type = null;
$entity_id = null;
$description = is_string($secondArg) ? $secondArg : null;
}
try {
$pdo->prepare("
INSERT INTO activity_logs (user_id, action, entity_type, entity_id, description, ip_address, user_agent, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
")->execute([
$_SESSION['user_id'],
$action,
$entity_type,
$entity_id,
$description,
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null,
]);
} catch (Throwable $e) {}
}
// ── DB change log (audit trail) ──────────────────────────────
function logDbChange(string $table, string $action, int $rowId, ?array $oldData = null, ?array $newData = null): void {
if (!isLoggedIn()) return;
global $pdo;
try {
$pdo->prepare("
INSERT INTO db_change_logs (user_id, table_name, action, row_id, ip_address, old_data, new_data)
VALUES (?, ?, ?, ?, ?, ?, ?)
")->execute([
$_SESSION['user_id'],
$table, $action, $rowId,
$_SERVER['REMOTE_ADDR'] ?? null,
$oldData ? json_encode($oldData) : null,
$newData ? json_encode($newData) : null,
]);
} catch (Throwable $e) {}
}
// ── Utility ─────────────────────────────────────────────────
function e(?string $s): string {
return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
}
function getSystemInfo(string $key, string $default = ''): string {
global $pdo;
try {
$stmt = $pdo->prepare("SELECT value FROM system_info WHERE `key` = ?");
$stmt->execute([$key]);
$r = $stmt->fetchColumn();
return $r !== false ? (string)$r : $default;
} catch (Throwable $e) { return $default; }
}
if (!function_exists('wp_branding_asset')) {
/**
* Resolve uploaded branding assets without hard-coding a company.
* Database paths are preferred, then files already present in assets/images/.
* Returns ['file' => string, 'disk' => string, 'url' => string] or null.
*/
function wp_branding_asset(string $type): ?array {
$type = strtolower(trim($type));
if (!in_array($type, ['logo', 'favicon'], true)) {
return null;
}
$imgDir = BASE_PATH . '/assets/images';
$tryFile = static function (?string $path) use ($imgDir): ?array {
$path = trim((string)$path);
if ($path === '') return null;
$path = strtok($path, '?') ?: $path;
$file = basename($path);
if ($file === '' || $file === '.' || $file === '..') return null;
$disk = $imgDir . '/' . $file;
if (!is_file($disk)) return null;
$ver = @filemtime($disk) ?: time();
return [
'file' => $file,
'disk' => $disk,
'url' => app_asset_url('images/' . $file) . '?v=' . $ver,
];
};
$fromDb = $tryFile(function_exists('getSystemInfo') ? getSystemInfo($type . '_path', '') : '');
if ($fromDb) return $fromDb;
$files = glob($imgDir . '/' . $type . '.*') ?: [];
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
foreach ($files as $full) {
$asset = $tryFile(basename($full));
if ($asset) return $asset;
}
return null;
}
}
if (!function_exists('wp_logo_asset')) {
function wp_logo_asset(): ?array {
return wp_branding_asset('logo');
}
}
if (!function_exists('wp_favicon_asset')) {
function wp_favicon_asset(bool $fallbackToLogo = true): ?array {
$fav = wp_branding_asset('favicon');
if ($fav) return $fav;
return $fallbackToLogo ? wp_branding_asset('logo') : null;
}
}
wp_enforce_clock_gate();
app_boot_output_buffer();
date_default_timezone_set('UTC');
if (defined('DEVELOPMENT') && DEVELOPMENT === true) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}