// 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 '
VIDEO
';
}, $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