$config['error']], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return; } // ---- Effective ONLYOFFICE values (constants override adminConfig) ---- $ooCfg = is_array($config['onlyoffice'] ?? null) ? $config['onlyoffice'] : []; $effEnabled = defined('ONLYOFFICE_ENABLED') ? (bool) ONLYOFFICE_ENABLED : (bool) ($ooCfg['enabled'] ?? false); $effDocs = defined('ONLYOFFICE_DOCS_ORIGIN') && ONLYOFFICE_DOCS_ORIGIN !== '' ? (string) ONLYOFFICE_DOCS_ORIGIN : (string) ($ooCfg['docsOrigin'] ?? ''); $hasSecret = defined('ONLYOFFICE_JWT_SECRET') ? (ONLYOFFICE_JWT_SECRET !== '') : (!empty($ooCfg['jwtSecret'])); $publicOriginCfg = (string) ($ooCfg['publicOrigin'] ?? ''); // Whitelisted public subset only (+ ONLYOFFICE enabled flag) $public = [ 'header_title' => (string)($config['header_title'] ?? 'FileRise'), 'loginOptions' => [ 'disableFormLogin' => (bool)($config['loginOptions']['disableFormLogin'] ?? false), 'disableBasicAuth' => (bool)($config['loginOptions']['disableBasicAuth'] ?? false), 'disableOIDCLogin' => (bool)($config['loginOptions']['disableOIDCLogin'] ?? false), ], 'globalOtpauthUrl' => (string)($config['globalOtpauthUrl'] ?? ''), 'enableWebDAV' => (bool)($config['enableWebDAV'] ?? false), 'sharedMaxUploadSize' => (int)($config['sharedMaxUploadSize'] ?? 0), 'oidc' => [ 'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''), 'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''), // never include clientId/clientSecret ], 'onlyoffice' => [ // Public only needs to know if it’s on; no secrets/origins here. 'enabled' => $effEnabled, ], ]; $isAdmin = !empty($_SESSION['authenticated']) && !empty($_SESSION['isAdmin']); if ($isAdmin) { // admin-only extras: presence flags + proxy options + ONLYOFFICE effective view $adminExtra = [ 'loginOptions' => array_merge($public['loginOptions'], [ 'authBypass' => (bool)($config['loginOptions']['authBypass'] ?? false), 'authHeaderName' => (string)($config['loginOptions']['authHeaderName'] ?? 'X-Remote-User'), ]), 'oidc' => array_merge($public['oidc'], [ 'hasClientId' => !empty($config['oidc']['clientId']), 'hasClientSecret' => !empty($config['oidc']['clientSecret']), ]), 'onlyoffice' => [ 'enabled' => $effEnabled, 'docsOrigin' => $effDocs, // effective (constants win) 'publicOrigin' => $publicOriginCfg, // optional override from adminConfig 'hasJwtSecret' => (bool)$hasSecret, // boolean only; never leak secret 'lockedByPhp' => ( defined('ONLYOFFICE_ENABLED') || defined('ONLYOFFICE_DOCS_ORIGIN') || defined('ONLYOFFICE_JWT_SECRET') ), ], ]; header('Cache-Control: no-store'); // don’t cache admin config echo json_encode(array_merge($public, $adminExtra), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return; } // Non-admins / unauthenticated: only the public subset header('Cache-Control: no-store'); echo json_encode($public, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return; } public function updateConfig(): void { header('Content-Type: application/json'); // —– auth & CSRF checks —– if ( !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || !isset($_SESSION['isAdmin']) || !$_SESSION['isAdmin'] ) { http_response_code(403); echo json_encode(['error' => 'Unauthorized access.']); exit; } $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); $receivedToken = trim($headersArr['x-csrf-token'] ?? ($_POST['csrfToken'] ?? '')); if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { http_response_code(403); echo json_encode(['error' => 'Invalid CSRF token.']); exit; } // —– fetch payload —– $data = json_decode(file_get_contents('php://input'), true); if (!is_array($data)) { http_response_code(400); echo json_encode(['error' => 'Invalid input.']); exit; } // —– load existing on-disk config —– $existing = AdminModel::getConfig(); if (isset($existing['error'])) { http_response_code(500); echo json_encode(['error' => $existing['error']]); exit; } // —– start merge with existing as base —– // Ensure minimal structure if the file was partially missing. $merged = $existing + [ 'header_title' => '', 'loginOptions' => [ 'disableFormLogin' => false, 'disableBasicAuth' => true, 'disableOIDCLogin' => true, 'authBypass' => false, 'authHeaderName' => 'X-Remote-User' ], 'globalOtpauthUrl' => '', 'enableWebDAV' => false, 'sharedMaxUploadSize' => 0, 'oidc' => [ 'providerUrl' => '', 'clientId' => '', 'clientSecret'=> '', 'redirectUri' => '' ], ]; // header_title (cap length and strip control chars) if (array_key_exists('header_title', $data)) { $title = trim((string)$data['header_title']); $title = preg_replace('/[\x00-\x1F\x7F]/', '', $title); if (mb_strlen($title) > 100) { // hard cap $title = mb_substr($title, 0, 100); } $merged['header_title'] = $title; } // loginOptions: inherit existing then override if provided foreach (['disableFormLogin','disableBasicAuth','disableOIDCLogin','authBypass'] as $flag) { if (isset($data['loginOptions'][$flag])) { $merged['loginOptions'][$flag] = filter_var( $data['loginOptions'][$flag], FILTER_VALIDATE_BOOLEAN ); } } if (isset($data['loginOptions']['authHeaderName'])) { $hdr = trim((string)$data['loginOptions']['authHeaderName']); // very restrictive header-name pattern: letters, numbers, dashes if ($hdr !== '' && preg_match('/^[A-Za-z0-9\-]+$/', $hdr)) { $merged['loginOptions']['authHeaderName'] = $hdr; } else { http_response_code(400); echo json_encode(['error' => 'Invalid authHeaderName.']); exit; } } // globalOtpauthUrl if (array_key_exists('globalOtpauthUrl', $data)) { $merged['globalOtpauthUrl'] = trim((string)$data['globalOtpauthUrl']); } // enableWebDAV if (array_key_exists('enableWebDAV', $data)) { $merged['enableWebDAV'] = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN); } // sharedMaxUploadSize if (array_key_exists('sharedMaxUploadSize', $data)) { $sms = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT); if ($sms === false || $sms < 0) { http_response_code(400); echo json_encode(['error' => 'sharedMaxUploadSize must be a non-negative integer (bytes).']); exit; } // Clamp to PHP limits to avoid confusing UX $maxPost = self::iniToBytes(ini_get('post_max_size')); $maxFile = self::iniToBytes(ini_get('upload_max_filesize')); $phpCap = min($maxPost ?: PHP_INT_MAX, $maxFile ?: PHP_INT_MAX); if ($phpCap !== PHP_INT_MAX && $sms > $phpCap) { $sms = $phpCap; } $merged['sharedMaxUploadSize'] = $sms; } // oidc: only overwrite non-empty inputs; validate when enabling OIDC foreach (['providerUrl','clientId','clientSecret','redirectUri'] as $f) { if (!empty($data['oidc'][$f])) { $val = trim((string)$data['oidc'][$f]); if ($f === 'providerUrl' || $f === 'redirectUri') { $val = filter_var($val, FILTER_SANITIZE_URL); } $merged['oidc'][$f] = $val; } } // If OIDC login is enabled, ensure required fields are present and sane $oidcEnabled = !empty($merged['loginOptions']['disableOIDCLogin']) ? false : true; if ($oidcEnabled) { $prov = $merged['oidc']['providerUrl'] ?? ''; $rid = $merged['oidc']['redirectUri'] ?? ''; $cid = $merged['oidc']['clientId'] ?? ''; // clientSecret may be empty for some PKCE-only flows, but commonly needed for code flow. if ($prov === '' || $rid === '' || $cid === '') { http_response_code(400); echo json_encode(['error' => 'OIDC is enabled but providerUrl, redirectUri, and clientId are required.']); exit; } // Require https except for localhost development $httpsOk = function(string $url): bool { if ($url === '') return false; $parts = parse_url($url); if (!$parts || empty($parts['scheme'])) return false; if ($parts['scheme'] === 'https') return true; if ($parts['scheme'] === 'http' && (isset($parts['host']) && ($parts['host'] === 'localhost' || $parts['host'] === '127.0.0.1'))) { return true; } return false; }; if (!$httpsOk($prov) || !$httpsOk($rid)) { http_response_code(400); echo json_encode(['error' => 'providerUrl and redirectUri must be https (or http on localhost)']); exit; } } // —– persist merged config —– // ---- ONLYOFFICE: merge from payload (unless locked by PHP defines) ---- $ooLockedByPhp = ( defined('ONLYOFFICE_ENABLED') || defined('ONLYOFFICE_DOCS_ORIGIN') || defined('ONLYOFFICE_JWT_SECRET') || defined('ONLYOFFICE_PUBLIC_ORIGIN') ); if (!$ooLockedByPhp && isset($data['onlyoffice']) && is_array($data['onlyoffice'])) { $ooExisting = (isset($existing['onlyoffice']) && is_array($existing['onlyoffice'])) ? $existing['onlyoffice'] : []; $oo = $ooExisting; if (array_key_exists('enabled', $data['onlyoffice'])) { $oo['enabled'] = filter_var($data['onlyoffice']['enabled'], FILTER_VALIDATE_BOOLEAN); } if (isset($data['onlyoffice']['docsOrigin'])) { $oo['docsOrigin'] = (string)$data['onlyoffice']['docsOrigin']; } if (isset($data['onlyoffice']['publicOrigin'])) { $oo['publicOrigin'] = (string)$data['onlyoffice']['publicOrigin']; } // Allow setting/changing the secret when NOT locked by PHP if (isset($data['onlyoffice']['jwtSecret'])) { $js = trim((string)$data['onlyoffice']['jwtSecret']); if ($js !== '') { $oo['jwtSecret'] = $js; // stored encrypted by AdminModel } // If blank, we leave existing secret unchanged (no implicit wipe). } $merged['onlyoffice'] = $oo; } $result = AdminModel::updateConfig($merged); if (isset($result['error'])) { http_response_code(500); } echo json_encode($result); exit; } /** Convert php.ini shorthand like "128M" to bytes */ private static function iniToBytes($val) { if ($val === false || $val === null || $val === '') return 0; $val = trim((string)$val); $last = strtolower($val[strlen($val)-1]); $num = (int)$val; switch ($last) { case 'g': $num *= 1024; case 'm': $num *= 1024; case 'k': $num *= 1024; } return $num; } } ?>