// Melhores Vagas Global Detection (Host-based is always safe) $host = $_SERVER['HTTP_HOST'] ?? ''; $isMelhoresVagas = (strpos($host, 'melhoresvagas.com.br') !== false); // We will refine this after session_start for the dev endpoint $GLOBALS['isMelhoresVagas'] = $isMelhoresVagas; // error_log("MV DEBUG: host=$host uri=$uri isMV=" . ($isMelhoresVagas ? 'YES' : 'NO')); // Basic startup: timezone, env loader (session will be started AFTER BASE_PATH) date_default_timezone_set('America/Sao_Paulo'); // Composer Autoloader if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) { require_once dirname(__DIR__) . '/vendor/autoload.php'; } // Error reporting based on APP_DEBUG $appDebug = env('APP_DEBUG', 'false'); if (strtolower((string)$appDebug) === 'true') { ini_set('display_errors', '1'); ini_set('display_startup_errors', '1'); error_reporting(E_ALL); } else { ini_set('display_errors', '0'); ini_set('display_startup_errors', '0'); } define('DOO_ROOT', dirname(__DIR__)); /** Build an app URL respecting BASE_PATH */ function url(string $path = '/'): string { $p = '/' . ltrim($path, '/'); if (BASE_PATH && BASE_PATH !== '/') return BASE_PATH . $p; return $p; } /** * Process text for Pulse Feed: converts @mentions, URLs, and video links (YouTube/Vimeo) */ function processPulseLinks($text) { // 1. YouTube Embedding $youtubeRegex = '/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[^\s]*)/i'; $text = preg_replace_callback($youtubeRegex, function($matches) { $videoId = $matches[1]; return '
'; }, $text); // 2. Vimeo Embedding $vimeoRegex = '/(?:https?:\/\/)?(?:www\.)?(?:vimeo\.com\/)([0-9]+)(?:[^\s]*)/i'; $text = preg_replace_callback($vimeoRegex, function($matches) { $videoId = $matches[1]; return '
'; }, $text); // 3. Convert @mentions $text = preg_replace('/@([a-zA-Z0-9_À-ÿ\s]+)/u', '@$1', htmlspecialchars($text)); // 4. Convert remaining URLs to clickable links (opening in new window) $urlRegex = '/(?'.$url.''; }, $text); return $text; } // Polyfills for string helper functions if running on older PHP (must be BEFORE loadEnv call) if (!function_exists('str_starts_with')) { function str_starts_with(string $haystack, string $needle): bool { return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0; } } if (!function_exists('str_ends_with')) { function str_ends_with(string $haystack, string $needle): bool { if ($needle === '') return true; $len = strlen($needle); return substr($haystack, -$len) === $needle; } } function loadEnv(string $file): void { if (!is_file($file)) return; $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (!$line || str_starts_with(trim($line), '#')) continue; $pos = strpos($line, '='); if ($pos === false) continue; $key = trim(substr($line, 0, $pos)); $val = trim(substr($line, $pos + 1)); // remove quotes if ((str_starts_with($val, '"') && str_ends_with($val, '"')) || (str_starts_with($val, "'") && str_ends_with($val, "'"))) { $val = substr($val, 1, -1); } $_ENV[$key] = $val; if (function_exists('putenv')) { putenv($key.'='.$val); } } } function env(string $key, $default = null) { static $dbConfigs = []; static $dbLoaded = false; // 1. Try environment / $_ENV first (bootstrap keys) $v = $_ENV[$key] ?? getenv($key); if ($v !== false && $v !== null) return $v; // 2. Avoid looking in DB for bootstrap keys itself (prevents circularity) $bootstrapKeys = [ 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'APP_ENV', 'APP_DEBUG', 'APP_URL', 'APP_BASE_PATH' ]; if (in_array($key, $bootstrapKeys)) return $default; // 3. Try database if not already loaded if (!$dbLoaded) { $dbLoaded = true; // Mark as loaded early to prevent recursion try { // Check if we have the minimum DB credentials to even try if (getenv('DB_HOST') || isset($_ENV['DB_HOST'])) { require_once __DIR__ . '/Database.php'; $pdo = Database::pdo(); $stmt = $pdo->query("SELECT cfg_key, cfg_value FROM sys_configs"); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $dbConfigs[$row['cfg_key']] = $row['cfg_value']; } } } catch (Throwable $e) { // Silently fail or log if DB is unavailable // error_log("ENV DB Error: " . $e->getMessage()); } } return $dbConfigs[$key] ?? $default; } // Load .env (prefer local .env over example) $envPath = DOO_ROOT . '/.env'; if (!is_file($envPath)) { $envPath = DOO_ROOT . '/.env.example'; } loadEnv($envPath); // Compute BASE_PATH after env is loaded if (!defined('BASE_PATH')) { // Explicit override via .env $forced = env('APP_BASE_PATH', null); if (is_string($forced) && $forced !== '') { $forced = str_replace('\\', '/', trim($forced)); if ($forced === '/' || $forced === '.') { $forced = ''; } if ($forced !== '' && $forced[0] !== '/') { $forced = '/'.$forced; } define('BASE_PATH', rtrim($forced, '/')); } else { // Prefer robust detection via DOCUMENT_ROOT vs application root directory $doc = isset($_SERVER['DOCUMENT_ROOT']) ? str_replace('\\', '/', rtrim((string)$_SERVER['DOCUMENT_ROOT'], '/')) : ''; $app = str_replace('\\', '/', rtrim(DOO_ROOT, '/')); $computed = ''; if ($doc !== '' && str_starts_with($app.'/', $doc.'/')) { $rel = substr($app, strlen($doc)); // leading '/subdir' or '' $computed = rtrim($rel, '/'); } else { // Fallback: infer from executing script (works for delegator subdirs) $scriptName = $_SERVER['SCRIPT_NAME'] ?? ''; if ($scriptName === '' || $scriptName === false) { $scriptName = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; } $bp = rtrim(str_replace('\\', '/', dirname($scriptName)), '/'); // Normalize when accessed through delegator subdirs if ($bp !== '' && $bp !== '/') { $last = basename($bp); if (in_array($last, ['login','signup','logout','dashboard'], true)) { $bp = rtrim(str_replace('\\', '/', dirname($bp)), '/'); } } if ($bp === '.' || $bp === '/') { $bp = ''; } $computed = $bp; } define('BASE_PATH', $computed); } } // Start session AFTER BASE_PATH is known so we can set proper cookie path if (session_status() === PHP_SESSION_NONE) { $sessName = env('SESSION_NAME', 'doo_session'); if ($sessName) { session_name($sessName); } // Use proper session driver $driver = env('SESSION_DRIVER', 'mysql'); if ($driver === 'redis') { require_once __DIR__.'/RedisSessionHandler.php'; if (class_exists('RedisSessionHandler')) { $handler = new RedisSessionHandler([ 'host' => env('REDIS_HOST', '127.0.0.1'), 'port' => (int)env('REDIS_PORT', 5432), 'user' => env('REDIS_USER'), 'pass' => env('REDIS_PASS'), ]); if (function_exists('session_set_save_handler')) { session_set_save_handler($handler, true); } } } else { // Use MySQL-backed sessions (no filesystem dependency) require_once __DIR__.'/DbSessionHandler.php'; if (class_exists('DbSessionHandler')) { $handler = new DbSessionHandler('sessions'); if (function_exists('session_set_save_handler')) { session_set_save_handler($handler, true); } } } // Strict and cookie-only sessions @ini_set('session.use_strict_mode', '1'); @ini_set('session.use_cookies', '1'); @ini_set('session.use_only_cookies', '1'); @ini_set('session.use_trans_sid', '0'); // Detect HTTPS accurately, even behind reverse proxies $xfp = strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')); $xssl = strtolower((string)($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '')); $isHttps = ($xfp === 'https') || ($xssl === 'on') || (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? '') == '443'); // Cookie settings: host-only, path '/', Lax, secure if HTTPS $cookiePath = '/'; $cookieDomain = env('SESSION_DOMAIN', ''); $cookieLifetime = 0; // session cookie // Suporte para Iframe no WhatsApp (embed=1 exige SameSite=None) $isEmbed = (isset($_GET['embed']) && $_GET['embed'] == '1') || (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] === 'iframe'); $sameSite = $isEmbed ? 'None' : 'Lax'; $secure = ($isHttps || $isEmbed) ? true : false; // SameSite=None exige Secure $params = [ 'lifetime' => $cookieLifetime, 'path' => $cookiePath, 'domain' => $cookieDomain, 'secure' => $secure, 'httponly' => true, 'samesite' => $sameSite, ]; if (function_exists('session_set_cookie_params')) { session_set_cookie_params($params); } // Ensure a logs directory exists and provide a simple logger $logDir = DOO_ROOT . '/storage/logs'; if (!is_dir($logDir)) { @mkdir($logDir, 0775, true); } if (!function_exists('doo_log')) { function doo_log(string $channel, string $message): void { $dir = DOO_ROOT . '/storage/logs'; if (!is_dir($dir)) { @mkdir($dir, 0775, true); } $file = $dir . '/' . preg_replace('/[^a-z0-9_\-]/i','_', $channel) . '.log'; $line = '['.date('Y-m-d H:i:s').'] ' . $message . "\n"; @file_put_contents($file, $line, FILE_APPEND); } } session_start(); if (isset($_SESSION['mv_dev_mode']) && $_SESSION['mv_dev_mode'] === true) { $GLOBALS['isMelhoresVagas'] = true; } // CSRF token for forms across the app if (!isset($_SESSION['csrf'])) { try { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } catch (Throwable $e) { $_SESSION['csrf'] = bin2hex(str_shuffle('abcdef0123456789')); } } // Initialize i18n system require_once __DIR__ . '/i18n.php'; i18n::init(); // Load permissions helper (must be after session start) require_once __DIR__ . '/permissions_helper.php'; // Load global Notification Service require_once __DIR__ . '/NotificationService.php'; // Update Session TTL if user has a custom preference if (isset($_SESSION['auth']) && isset($handler) && $handler instanceof RedisSessionHandler) { // We only check DB occasionally or if not set in session to avoid query every hit? // For simplicity, let's trust the session data if we put it there, or fetch once. // Actually, let's fetch from DB to be sure on critical updates, but cache in session? // Let's rely on what's in $_SESSION['auth']['session_lifetime'] if we store it there, // OR fetch it if missing. But bootstrap runs on every request. // Optimization: user session lifetime is likely static per login. // We can fetch it once during login/refresh. // BUT current bootstrap doesn't know if we just logged in. // Let's check if we have a special key in session, else fetch. $customTtl = $_SESSION['auth']['session_lifetime'] ?? null; // If not in session, try to get from DB (lightweight) if ($customTtl === null && !empty($_SESSION['auth']['id'])) { try { // We reuse the existing $pdo connection if DbSessionHandler was used, // but here we might need a new one if not available. // Database::pdo() is singleton-ish. $pdoTtl = Database::pdo(); $stmTtl = $pdoTtl->prepare("SELECT session_lifetime FROM users WHERE id = ?"); $stmTtl->execute([$_SESSION['auth']['id']]); $customTtl = $stmTtl->fetchColumn(); // Cache it back to session to avoid query next time? // Careful: modifying session in bootstrap might trigger write $_SESSION['auth']['session_lifetime'] = $customTtl; } catch (Throwable $e) {} } // Apply if set (and valid > 0) if ($customTtl && (int)$customTtl > 0) { $handler->setTtl((int)$customTtl); // Also update cookie params if we want the cookie to last that long? // "session.cookie_lifetime" is usually 0 (until browser close) for security, // but if user asks for "7 days", we might need persistent cookie. // NOTE: Changing cookie params after session_start has no effect on the current cookie, // but we can send a new Set-Cookie header. // However, typical "Remember Me" logic is: // High Session TTL + High Cookie TTL. // If customTtl > standard session, we imply persistent login. // 0 means "browser session" for cookie, but server session needs to last. // Let's respect "Never" (31536000) or strict days. // If it's a long duration, we likely want a persistent cookie. if ((int)$customTtl > 86400) { // More than 24h $params = session_get_cookie_params(); setcookie( session_name(), session_id(), [ 'expires' => time() + (int)$customTtl, 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'], ] ); } } } } function jsonResponse($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); exit; } function requireMethod(string $method): void { if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== strtoupper($method)) { jsonResponse(['error' => 'Method not allowed'], 405); } } /** * Exigir autenticação do usuário. * Redireciona para login se for acesso web ou retorna 401 JSON se for API/AJAX. */ if (!function_exists('requireAuth')) { function requireAuth(): void { // Tentar autenticação via token na URL se for embed if (!isset($_SESSION['auth']) && isset($_GET['token']) && strlen($_GET['token']) > 20) { $token = $_GET['token']; try { require_once __DIR__ . '/Database.php'; $pdoAuth = Database::pdo(); $stAuth = $pdoAuth->prepare("SELECT id, name, email, company_id, role, avatar FROM users WHERE ext_token = ? AND deleted_at IS NULL LIMIT 1"); $stAuth->execute([$token]); $userAuth = $stAuth->fetch(PDO::FETCH_ASSOC); if ($userAuth) { $_SESSION['auth'] = [ 'id' => $userAuth['id'], 'name' => $userAuth['name'], 'email' => $userAuth['email'], 'company_id' => $userAuth['company_id'], 'role' => $userAuth['role'], 'avatar' => $userAuth['avatar'] ]; // log auto-login doo_log('auth', "Auto-login via iframe token for user {$userAuth['id']}"); } } catch (Throwable $e) { doo_log('error', "Auto-login failed: " . $e->getMessage()); } } if (!isset($_SESSION['auth'])) { // Verificar se é requisição AJAX ou API $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; $isApi = str_starts_with($_SERVER['REQUEST_URI'] ?? '', BASE_PATH . '/api/'); if ($isAjax || $isApi) { jsonResponse(['success' => false, 'error' => 'Authentication required', 'code' => 401], 401); } else { header('Location: ' . url('/login') . (isset($_GET['embed']) ? '?embed=1' : '')); exit; } } // Se for embed, garantir que não estamos bloqueando iframe if (isset($_GET['embed']) && $_GET['embed'] == '1') { header_remove('X-Frame-Options'); header("Content-Security-Policy: frame-ancestors 'self' https://web.whatsapp.com https://*.whatsapp.net"); } } } /** * Exigir autenticação especificamente para APIs (sempre retorna JSON) */ if (!function_exists('requireAuthApi')) { function requireAuthApi(): void { if (!isset($_SESSION['auth'])) { jsonResponse(['success' => false, 'error' => 'Authentication required', 'code' => 401], 401); } } } function cors(): void { // Simple CORS for APIs (adjust as needed) header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET,POST,OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { exit; } } // THEME helpers if (!function_exists('escp')) { /** * Safe HTML escape for printing. * Usage: echo escp($value); */ function escp($value): string { if ($value === null) return ''; return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } } function theme_name(): string { return (string)env('APP_THEME', 'padrao'); } function theme_path(string $relative): string { $t = theme_name(); return DOO_ROOT.'/themes/'.$t.'/'.ltrim($relative, '/'); } function theme_config(): array { static $cfg = null; if ($cfg !== null) return $cfg; $file = theme_path('config.php'); $cfg = is_file($file) ? (include $file) : []; return is_array($cfg) ? $cfg : []; } function render_theme(string $part, array $vars = []): void { $file = theme_path($part.'.php'); if (!is_file($file)) return; $config = theme_config(); extract($vars); include $file; } /** * Renderiza as tags de favicon padrão do Doost */ function render_favicon(): void { $faviconPath = url('/favicon.png'); echo << HTML; } if (!function_exists('render_google_analytics')) { function render_google_analytics(): void { $gaId = env('GOOGLE_ANALYTICS_ID', 'G-NSXDT2EQM8'); if ($gaId) { echo " "; if (isset($_GET['registered']) && $_GET['registered'] === 'true') { echo ""; } } } } if (!function_exists('view')) { function view(string $path, array $data = []) { extract($data); include $path; } } if (!function_exists('brl')) { function brl($n) { $n = (float)($n ?? 0); return 'R$ ' . number_format($n, 2, ',', '.'); } } // ============================================================================ // PERMISSIONS SYSTEM HELPERS // ============================================================================ /** * Verificar se usuário atual tem permissão * * @param string $module Nome do módulo (ex: 'marketing.branding') * @param string $action Ação (view, create, edit, delete) * @return bool */ /** * DEPRECATED: Moved to src/permissions_helper.php * Old version without admin bypass - DO NOT USE */ /* function hasPermission($module, $action = 'view') { global $me; if (!$me) return false; require_once __DIR__ . '/PermissionsManager.php'; require_once __DIR__ . '/Database.php'; static $pm = null; if ($pm === null) { $pm = new PermissionsManager(Database::pdo()); } return $pm->hasPermission($me['id'], $module, $action); } */ /** * Exigir permissão (retorna 403 se não tiver) * * @param string $module * @param string $action */ function requirePermission($module, $action = 'view') { if (!hasPermission($module, $action)) { http_response_code(403); // Se for requisição AJAX/API, retornar JSON if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { header('Content-Type: application/json'); echo json_encode(['error' => 'Permissão negada', 'module' => $module, 'action' => $action]); exit; } // Senão, mostrar página de erro echo '

403 - Acesso Negado

'; echo '

Você não tem permissão para acessar este recurso.

'; echo '

Voltar ao Dashboard

'; exit; } } /** * Verificar se pode editar usuário * * @param array $me Usuário atual * @param int $targetUserId ID do usuário a ser editado * @return bool */ function canEditUser($me, $targetUserId) { error_log("=== canEditUser DEBUG ==="); error_log("Target User ID: " . $targetUserId); if (!$me) { error_log("NEGADO: \$me é null"); return false; } error_log("Current User ID: " . $me['id']); error_log("Current User Email: " . ($me['email'] ?? 'N/A')); // Usuário pode editar a si mesmo if ($me['id'] == $targetUserId) { error_log("PERMITIDO: Editando a si mesmo"); return true; } // Super admin pode editar qualquer um (se a coluna roles existir e for válida) if (isset($me['roles'])) { error_log("Coluna roles existe. Tipo: " . gettype($me['roles'])); // Roles pode ser array (já decodificado na sessão) ou string JSON if (is_array($me['roles'])) { error_log("roles já é array PHP"); $roles = $me['roles']; } else { error_log("roles é string, tentando decodificar. Valor: " . substr($me['roles'], 0, 100)); // Verificar se é JSON válido $roles = @json_decode($me['roles'], true); error_log("JSON decode result: " . (is_array($roles) ? json_encode($roles) : 'FALHOU')); if (!is_array($roles)) { error_log("JSON inválido, usando array vazio"); $roles = []; } } if (is_array($roles) && in_array('super_admin', $roles, true)) { error_log("PERMITIDO: É super_admin"); return true; } } else { error_log("Coluna roles NÃO existe na sessão"); } // Se não tem coluna roles ainda, permitir edição para company admins if (isset($me['company_id'])) { error_log("Verificando company_id. Current: " . $me['company_id']); // Verificar se é admin da empresa require_once __DIR__ . '/Database.php'; $pdo = Database::pdo(); $stmt = $pdo->prepare("SELECT company_id FROM users WHERE id = ?"); $stmt->execute([$targetUserId]); $targetCompany = $stmt->fetchColumn(); error_log("Target company_id: " . ($targetCompany ?: 'NULL')); // Pode editar se for da mesma empresa if ($targetCompany == $me['company_id']) { error_log("PERMITIDO: Mesma empresa"); return true; } else { error_log("NEGADO: Empresas diferentes"); } } else { error_log("company_id não existe na sessão"); } // Outros casos: verificar permissão específica (se sistema de permissões estiver ativo) error_log("Tentando hasPermission('users', 'edit')"); try { $result = hasPermission('users', 'edit'); error_log("hasPermission result: " . ($result ? 'true' : 'false')); return $result; } catch (Exception $e) { error_log("hasPermission exception: " . $e->getMessage()); // Se der erro (tabela não existe), permitir por padrão error_log("PERMITIDO: Fallback (tabela não existe)"); return true; } }
Warning: http_response_code(): Cannot set response code - headers already sent (output started at /www/wwwroot/doost.online/src/bootstrap.php:1) in /www/wwwroot/doost.online/router.php on line 11682
Página não encontrada