mirror of
https://github.com/error311/FileRise.git
synced 2026-05-17 01:19:36 -05:00
500 lines
18 KiB
PHP
500 lines
18 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
// config.php
|
||
|
||
// Define constants
|
||
define('PROJECT_ROOT', dirname(__DIR__));
|
||
$testUploadDir = getenv('FR_TEST_UPLOAD_DIR');
|
||
$testUsersDir = getenv('FR_TEST_USERS_DIR');
|
||
$testMetaDir = getenv('FR_TEST_META_DIR');
|
||
define(
|
||
'UPLOAD_DIR',
|
||
($testUploadDir !== false && $testUploadDir !== '')
|
||
? rtrim($testUploadDir, "/\\") . '/'
|
||
: '/var/www/uploads/'
|
||
);
|
||
define(
|
||
'USERS_DIR',
|
||
($testUsersDir !== false && $testUsersDir !== '')
|
||
? rtrim($testUsersDir, "/\\") . '/'
|
||
: '/var/www/users/'
|
||
);
|
||
define('USERS_FILE', 'users.txt');
|
||
define(
|
||
'META_DIR',
|
||
($testMetaDir !== false && $testMetaDir !== '')
|
||
? rtrim($testMetaDir, "/\\") . '/'
|
||
: '/var/www/metadata/'
|
||
);
|
||
define('META_FILE', 'file_metadata.json');
|
||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||
define('TIMEZONE', 'America/New_York');
|
||
define('DATE_TIME_FORMAT','m/d/y h:iA');
|
||
define('TOTAL_UPLOAD_SIZE','5G');
|
||
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
|
||
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||
define('FR_DEMO_MODE', false);
|
||
|
||
date_default_timezone_set(TIMEZONE);
|
||
|
||
if (!defined('DEFAULT_BYPASS_OWNERSHIP')) define('DEFAULT_BYPASS_OWNERSHIP', false);
|
||
if (!defined('DEFAULT_CAN_SHARE')) define('DEFAULT_CAN_SHARE', true);
|
||
if (!defined('DEFAULT_CAN_ZIP')) define('DEFAULT_CAN_ZIP', true);
|
||
if (!defined('DEFAULT_VIEW_OWN_ONLY')) define('DEFAULT_VIEW_OWN_ONLY', false);
|
||
define('FOLDER_OWNERS_FILE', META_DIR . 'folder_owners.json');
|
||
define('ACL_INHERIT_ON_CREATE', true);
|
||
// ONLYOFFICE integration overrides (uncomment and set as needed)
|
||
/*
|
||
define('ONLYOFFICE_ENABLED', false);
|
||
define('ONLYOFFICE_JWT_SECRET', 'test123456');
|
||
define('ONLYOFFICE_DOCS_ORIGIN', 'http://192.168.1.61'); // your Document Server
|
||
define('ONLYOFFICE_DEBUG', true);
|
||
*/
|
||
if (!defined('OFFICE_SNIPPET_MAX_BYTES')) {
|
||
define('OFFICE_SNIPPET_MAX_BYTES', 5 * 1024 * 1024); // 5 MiB
|
||
}
|
||
|
||
if (!defined('OIDC_TOKEN_ENDPOINT_AUTH_METHOD')) {
|
||
define('OIDC_TOKEN_ENDPOINT_AUTH_METHOD', 'client_secret_basic'); // default
|
||
}
|
||
// --- Optional: OIDC → FileRise integration ----------------------------
|
||
|
||
// Auto-create users from OIDC when no users.txt match.
|
||
if (!defined('FR_OIDC_AUTO_CREATE')) {
|
||
define('FR_OIDC_AUTO_CREATE', true);
|
||
}
|
||
|
||
// Claim that contains IdP groups/roles (typical: "groups" or "roles").
|
||
if (!defined('FR_OIDC_GROUP_CLAIM')) {
|
||
define('FR_OIDC_GROUP_CLAIM', 'groups');
|
||
}
|
||
|
||
// Name of an IdP group that should be treated as "FileRise admin".
|
||
if (!defined('FR_OIDC_ADMIN_GROUP')) {
|
||
define('FR_OIDC_ADMIN_GROUP', 'filerise-admins');
|
||
}
|
||
|
||
// Prefix for IdP groups that should map into FileRise Pro groups.
|
||
// Example: IdP group "frp_clients_acme" → Pro group "clients_acme".
|
||
if (!defined('FR_OIDC_PRO_GROUP_PREFIX')) {
|
||
define('FR_OIDC_PRO_GROUP_PREFIX', '');
|
||
}
|
||
// Optional env/constant override: if set, it wins; if not set, UI setting is used.
|
||
if (!defined('FR_OIDC_ALLOW_DEMOTE')) {
|
||
$envVal = getenv('FR_OIDC_ALLOW_DEMOTE');
|
||
|
||
if ($envVal !== false && $envVal !== '') {
|
||
$val = strtolower(trim((string)$envVal));
|
||
define('FR_OIDC_ALLOW_DEMOTE', $val === '1' || $val === 'true');
|
||
}
|
||
// IMPORTANT: no "else" here ⇒ if env is not set, we do NOT define the constant,
|
||
// so AuthModel::isOidcDemoteAllowed() will fall back to AdminModel::getConfig().
|
||
}
|
||
if (!defined('FR_OIDC_DEBUG')) {
|
||
$envVal = getenv('FR_OIDC_DEBUG');
|
||
if ($envVal !== false && $envVal !== '') {
|
||
$val = strtolower(trim((string)$envVal));
|
||
define('FR_OIDC_DEBUG', in_array($val, ['1', 'true', 'yes', 'on'], true));
|
||
} else {
|
||
define('FR_OIDC_DEBUG', false);
|
||
}
|
||
}
|
||
// Antivirus / ClamAV (optional)
|
||
// If VIRUS_SCAN_ENABLED is set in the environment, it overrides the admin setting.
|
||
// If it is not set, we don't define the constant and the admin checkbox controls scanning.
|
||
$envScanRaw = getenv('VIRUS_SCAN_ENABLED');
|
||
if ($envScanRaw !== false && $envScanRaw !== '') {
|
||
$val = strtolower(trim((string)$envScanRaw));
|
||
$enabled = in_array($val, ['1', 'true', 'yes', 'on'], true);
|
||
define('VIRUS_SCAN_ENABLED', $enabled);
|
||
}
|
||
|
||
// Which scanner command to run. Can be "clamscan" or "clamdscan" (faster with clamd).
|
||
define('VIRUS_SCAN_CMD', getenv('VIRUS_SCAN_CMD') ?: 'clamscan');
|
||
|
||
// Optional: max time you consider acceptable for a scan (for logging / future timeout logic)
|
||
define('VIRUS_SCAN_TIMEOUT', 60);
|
||
|
||
// Encryption helpers
|
||
function encryptData($data, $encryptionKey)
|
||
{
|
||
$cipher = 'AES-256-CBC';
|
||
$ivlen = openssl_cipher_iv_length($cipher);
|
||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||
$ct = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||
return base64_encode($iv . $ct);
|
||
}
|
||
|
||
function decryptData($encryptedData, $encryptionKey)
|
||
{
|
||
$cipher = 'AES-256-CBC';
|
||
$data = base64_decode($encryptedData);
|
||
$ivlen = openssl_cipher_iv_length($cipher);
|
||
$iv = substr($data, 0, $ivlen);
|
||
$ct = substr($data, $ivlen);
|
||
return openssl_decrypt($ct, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||
}
|
||
|
||
// Load encryption key
|
||
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||
if ($envKey === false || $envKey === '') {
|
||
$encryptionKey = 'default_please_change_this_key';
|
||
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
|
||
} else {
|
||
$encryptionKey = $envKey;
|
||
}
|
||
|
||
// Helper to load JSON permissions (with optional decryption)
|
||
function loadUserPermissions($username)
|
||
{
|
||
global $encryptionKey;
|
||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||
if (!file_exists($permissionsFile)) {
|
||
return false;
|
||
}
|
||
|
||
$content = file_get_contents($permissionsFile);
|
||
$decrypted = decryptData($content, $encryptionKey);
|
||
$json = ($decrypted !== false) ? $decrypted : $content;
|
||
$permsAll = json_decode($json, true);
|
||
|
||
if (!is_array($permsAll)) {
|
||
return false;
|
||
}
|
||
|
||
// Try exact match first, then lowercase (since we store keys lowercase elsewhere)
|
||
$uExact = (string)$username;
|
||
$uLower = strtolower($uExact);
|
||
|
||
$row = $permsAll[$uExact] ?? $permsAll[$uLower] ?? null;
|
||
|
||
// Normalize: always return an array when found, else false (to preserve current callers’ behavior)
|
||
return is_array($row) ? $row : false;
|
||
}
|
||
|
||
// Determine HTTPS usage
|
||
$envSecure = getenv('SECURE');
|
||
$secure = ($envSecure !== false)
|
||
? filter_var($envSecure, FILTER_VALIDATE_BOOLEAN)
|
||
: (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||
|
||
|
||
// PHP session lifetime (independent of "remember me")
|
||
// Keep this reasonably short; "remember me" uses its own token.
|
||
$defaultSession = 7200; // 2 hours
|
||
$sessionLifetime = $defaultSession;
|
||
|
||
// "Remember me" window (how long the persistent token itself is valid)
|
||
// This is used in persistent_tokens.json, *not* for PHP session lifetime.
|
||
$persistentDays = 30 * 24 * 60 * 60; // 30 days
|
||
|
||
/**
|
||
* Start session idempotently:
|
||
* - If no session: set cookie params + gc_maxlifetime, then session_start().
|
||
* - If session already active: DO NOT change ini/cookie params; optionally refresh cookie expiry.
|
||
*/
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
session_set_cookie_params([
|
||
'lifetime' => $sessionLifetime,
|
||
'path' => '/',
|
||
'domain' => '', // adjust if you need a specific domain
|
||
'secure' => $secure,
|
||
'httponly' => true,
|
||
'samesite' => 'Lax'
|
||
]);
|
||
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||
session_start();
|
||
} else {
|
||
// Optionally refresh the session cookie expiry to keep the user alive
|
||
$params = session_get_cookie_params();
|
||
if ($sessionLifetime > 0) {
|
||
setcookie(session_name(), session_id(), [
|
||
'expires' => time() + $sessionLifetime,
|
||
'path' => $params['path'] ?: '/',
|
||
'domain' => $params['domain'] ?? '',
|
||
'secure' => $secure,
|
||
'httponly' => true,
|
||
'samesite' => $params['samesite'] ?? 'Lax',
|
||
]);
|
||
}
|
||
}
|
||
|
||
// CSRF token
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
|
||
// Auto-login via persistent token
|
||
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||
$payload = AuthModel::consumeRememberToken($_COOKIE['remember_me_token']);
|
||
if ($payload) {
|
||
// NEW: mitigate session fixation
|
||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||
session_regenerate_id(true);
|
||
}
|
||
|
||
$_SESSION["authenticated"] = true;
|
||
$_SESSION["username"] = $payload["username"];
|
||
$perms = loadUserPermissions($payload["username"]);
|
||
$_SESSION["folderOnly"] = $perms['folderOnly'] ?? false;
|
||
$_SESSION["readOnly"] = $perms['readOnly'] ?? false;
|
||
$_SESSION["disableUpload"] = $perms['disableUpload'] ?? false;
|
||
$_SESSION["isAdmin"] = !empty($payload["isAdmin"]);
|
||
|
||
if (!empty($payload['token']) && !empty($payload['expiry'])) {
|
||
setcookie('remember_me_token', $payload['token'], (int)$payload['expiry'], '/', '', $secure, true);
|
||
}
|
||
} else {
|
||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||
}
|
||
}
|
||
|
||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||
|
||
// sane defaults:
|
||
$cfgAuthBypass = false;
|
||
$cfgAuthHeader = 'X_REMOTE_USER';
|
||
|
||
if (file_exists($adminConfigFile)) {
|
||
$encrypted = file_get_contents($adminConfigFile);
|
||
$decrypted = decryptData($encrypted, $encryptionKey);
|
||
$adminCfg = json_decode($decrypted, true) ?: [];
|
||
|
||
$loginOpts = $adminCfg['loginOptions'] ?? [];
|
||
|
||
// proxy-only bypass flag
|
||
$cfgAuthBypass = ! empty($loginOpts['authBypass']);
|
||
|
||
// header name (e.g. “X-Remote-User” → HTTP_X_REMOTE_USER)
|
||
$hdr = trim($loginOpts['authHeaderName'] ?? '');
|
||
if ($hdr === '') {
|
||
$hdr = 'X-Remote-User';
|
||
}
|
||
// normalize to PHP’s $_SERVER key format:
|
||
$cfgAuthHeader = 'HTTP_' . strtoupper(str_replace('-', '_', $hdr));
|
||
}
|
||
|
||
define('AUTH_BYPASS', $cfgAuthBypass);
|
||
define('AUTH_HEADER', $cfgAuthHeader);
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// PROXY-ONLY AUTO–LOGIN now uses those constants:
|
||
if (AUTH_BYPASS) {
|
||
$hdrKey = AUTH_HEADER; // e.g. "HTTP_X_REMOTE_USER"
|
||
if (!empty($_SERVER[$hdrKey])) {
|
||
// regenerate once per session
|
||
if (empty($_SESSION['authenticated'])) {
|
||
session_regenerate_id(true);
|
||
}
|
||
|
||
$username = $_SERVER[$hdrKey];
|
||
$_SESSION['authenticated'] = true;
|
||
$_SESSION['username'] = $username;
|
||
|
||
// ◾ lookup actual role instead of forcing admin
|
||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||
$role = AuthModel::getUserRole($username);
|
||
$_SESSION['isAdmin'] = ($role === '1');
|
||
|
||
// carry over any folder/read/upload perms
|
||
$perms = loadUserPermissions($username) ?: [];
|
||
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||
}
|
||
}
|
||
|
||
// Share URL fallback (keep BASE_URL behavior)
|
||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||
|
||
// Detect scheme correctly (works behind proxies too)
|
||
$proto = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? (
|
||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'
|
||
);
|
||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||
|
||
// Base path support (optional):
|
||
// - If FileRise is served under a subpath (e.g. https://example.com/fr),
|
||
// set `FR_BASE_PATH=/fr` or send `X-Forwarded-Prefix: /fr` from the proxy.
|
||
// - When not set, defaults to "" (root install) to preserve existing behavior.
|
||
function fr_normalize_base_path($raw)
|
||
{
|
||
$p = trim((string)$raw);
|
||
if ($p === '' || $p === '/') return '';
|
||
// Reject full URLs or scheme-relative prefixes to avoid open redirects.
|
||
if (preg_match('~^[a-z][a-z0-9+.-]*://~i', $p)) return '';
|
||
if (strpos($p, '//') === 0) return '';
|
||
// Normalize slashes and strip query/fragment if provided.
|
||
$p = str_replace('\\', '/', $p);
|
||
$p = preg_replace('/[?#].*$/', '', $p);
|
||
if ($p === '' || $p === '/') return '';
|
||
if ($p[0] !== '/') $p = '/' . $p;
|
||
$p = preg_replace('~/+~', '/', $p);
|
||
// Disallow path traversal segments.
|
||
if (preg_match('~(^|/)\.\.(?:/|$)~', $p)) return '';
|
||
// strip trailing slashes
|
||
return preg_replace('~/+$~', '', $p) ?: '';
|
||
}
|
||
|
||
function fr_detect_base_path()
|
||
{
|
||
// 1) Explicit env override
|
||
$env = getenv('FR_BASE_PATH');
|
||
if ($env !== false && trim((string)$env) !== '') {
|
||
return fr_normalize_base_path($env);
|
||
}
|
||
|
||
// 2) Reverse proxies often provide this
|
||
$xfp = $_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '';
|
||
if (is_string($xfp) && trim($xfp) !== '') {
|
||
return fr_normalize_base_path($xfp);
|
||
}
|
||
|
||
// 3) If deployed in a real subdirectory (not stripped by proxy),
|
||
// SCRIPT_NAME/REQUEST_URI will include the prefix (e.g. /fr/api/...).
|
||
$candidates = [];
|
||
if (!empty($_SERVER['SCRIPT_NAME']) && is_string($_SERVER['SCRIPT_NAME'])) $candidates[] = $_SERVER['SCRIPT_NAME'];
|
||
if (!empty($_SERVER['REQUEST_URI']) && is_string($_SERVER['REQUEST_URI'])) {
|
||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||
if (is_string($path) && $path !== '') $candidates[] = $path;
|
||
}
|
||
|
||
foreach ($candidates as $p) {
|
||
if (preg_match('~^(.*)/api(?:/|\\.php$)~i', $p, $m)) return fr_normalize_base_path($m[1]);
|
||
if (preg_match('~^(.*)/webdav\\.php$~i', $p, $m)) return fr_normalize_base_path($m[1]);
|
||
if (preg_match('~^(.*)/index\\.html$~i', $p, $m)) return fr_normalize_base_path($m[1]);
|
||
if (preg_match('~^(.*)/portal-login\\.html$~i', $p, $m)) return fr_normalize_base_path($m[1]);
|
||
if (preg_match('~^(.*)/portal\\.html$~i', $p, $m)) return fr_normalize_base_path($m[1]);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
if (!defined('FR_BASE_PATH')) {
|
||
define('FR_BASE_PATH', fr_detect_base_path());
|
||
}
|
||
|
||
function fr_with_base_path($path)
|
||
{
|
||
$p = (string)$path;
|
||
$bp = defined('FR_BASE_PATH') ? (string)FR_BASE_PATH : '';
|
||
if ($bp === '' || $p === '' || $p[0] !== '/') return $p;
|
||
if ($p === $bp || strpos($p, $bp . '/') === 0) return $p;
|
||
return $bp . $p;
|
||
}
|
||
|
||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||
$defaultShare = "{$proto}://{$host}" . fr_with_base_path("/api/file/share.php");
|
||
} else {
|
||
$defaultShare = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
||
}
|
||
|
||
// Final: env var wins, else fallback
|
||
// Optional: Published URL override (preferred: env, optional: admin config).
|
||
// This is the canonical URL FileRise should advertise (e.g. "https://example.com/fr").
|
||
function fr_sanitize_http_url($url)
|
||
{
|
||
$u = trim((string)$url);
|
||
if ($u === '') return '';
|
||
if (!filter_var($u, FILTER_VALIDATE_URL)) return '';
|
||
$scheme = strtolower(parse_url($u, PHP_URL_SCHEME) ?: '');
|
||
if ($scheme !== 'http' && $scheme !== 'https') return '';
|
||
return $u;
|
||
}
|
||
|
||
function fr_read_admin_config_raw(): array
|
||
{
|
||
try {
|
||
$configFile = USERS_DIR . 'adminConfig.json';
|
||
if (!is_file($configFile)) return [];
|
||
$encryptedContent = @file_get_contents($configFile);
|
||
if (!is_string($encryptedContent) || $encryptedContent === '') return [];
|
||
$dec = decryptData($encryptedContent, $GLOBALS['encryptionKey']);
|
||
if ($dec === false) return [];
|
||
$cfg = json_decode($dec, true);
|
||
return is_array($cfg) ? $cfg : [];
|
||
} catch (Throwable $e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
$envPublished = getenv('FR_PUBLISHED_URL');
|
||
$published = '';
|
||
if ($envPublished !== false && trim((string)$envPublished) !== '') {
|
||
$published = fr_sanitize_http_url($envPublished);
|
||
} else {
|
||
$adminCfg = fr_read_admin_config_raw();
|
||
$published = fr_sanitize_http_url($adminCfg['publishedUrl'] ?? '');
|
||
}
|
||
|
||
if (!defined('FR_PUBLISHED_URL_EFFECTIVE')) {
|
||
define('FR_PUBLISHED_URL_EFFECTIVE', $published);
|
||
}
|
||
|
||
$envShare = getenv('SHARE_URL');
|
||
if ($envShare !== false && trim((string)$envShare) !== '') {
|
||
define('SHARE_URL', (string)$envShare);
|
||
} elseif ($published !== '') {
|
||
define('SHARE_URL', rtrim($published, '/') . '/api/file/share.php');
|
||
} else {
|
||
define('SHARE_URL', $defaultShare);
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// FileRise Pro bootstrap wiring
|
||
// ------------------------------------------------------------
|
||
|
||
// Inline license (optional; usually set via Admin UI and PRO_LICENSE_FILE)
|
||
if (!defined('FR_PRO_LICENSE')) {
|
||
$envLicense = getenv('FR_PRO_LICENSE');
|
||
define('FR_PRO_LICENSE', $envLicense !== false ? trim((string)$envLicense) : '');
|
||
}
|
||
|
||
// JSON license file used by AdminController::setLicense()
|
||
if (!defined('PRO_LICENSE_FILE')) {
|
||
define('PRO_LICENSE_FILE', rtrim(USERS_DIR, "/\\") . '/proLicense.json');
|
||
}
|
||
|
||
// Optional plain-text license file (used as fallback in bootstrap)
|
||
if (!defined('FR_PRO_LICENSE_FILE')) {
|
||
$lf = getenv('FR_PRO_LICENSE_FILE');
|
||
if ($lf === false || $lf === '') {
|
||
$lf = rtrim(USERS_DIR, "/\\") . '/proLicense.txt';
|
||
}
|
||
define('FR_PRO_LICENSE_FILE', $lf);
|
||
}
|
||
|
||
// Where Pro code lives by default → inside users volume
|
||
$proDir = getenv('FR_PRO_BUNDLE_DIR');
|
||
if ($proDir === false || $proDir === '') {
|
||
$proDir = rtrim(USERS_DIR, "/\\") . '/pro';
|
||
}
|
||
$proDir = rtrim($proDir, "/\\");
|
||
if (!defined('FR_PRO_BUNDLE_DIR')) {
|
||
define('FR_PRO_BUNDLE_DIR', $proDir);
|
||
}
|
||
|
||
// Try to load Pro bootstrap if enabled + present
|
||
$proBootstrap = FR_PRO_BUNDLE_DIR . '/bootstrap_pro.php';
|
||
if (@is_file($proBootstrap)) {
|
||
require_once $proBootstrap;
|
||
}
|
||
|
||
// If bootstrap didn’t define these, give safe defaults
|
||
if (!defined('FR_PRO_ACTIVE')) {
|
||
define('FR_PRO_ACTIVE', false);
|
||
}
|
||
if (!defined('FR_PRO_INFO')) {
|
||
define('FR_PRO_INFO', [
|
||
'valid' => false,
|
||
'error' => null,
|
||
'payload' => null,
|
||
]);
|
||
}
|
||
if (!defined('FR_PRO_BUNDLE_VERSION')) {
|
||
define('FR_PRO_BUNDLE_VERSION', null);
|
||
}
|