BROOKO icon
BROOKO UK NETWORK
Where code meets creativity & adventure
File viewer

bootstrap.php

Type
php
Size
13.99 KB
Modified
15 May
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);
}