release(v1.9.8): feat(pro): wire core to Pro licensing + branding hooks
This commit is contained in:
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,75 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 11/16/2025 (v1.9.8)
|
||||
|
||||
release(v1.9.8): feat(pro): wire core to Pro licensing + branding hooks
|
||||
|
||||
- Add Pro feature flags + bootstrap wiring
|
||||
- Define FR_PRO_ACTIVE/FR_PRO_TYPE/FR_PRO_EMAIL/FR_PRO_VERSION/FR_PRO_LICENSE_FILE
|
||||
in config.php and optionally require src/pro/bootstrap_pro.php.
|
||||
- Expose a `pro` block from AdminController::getConfig() so the UI can show
|
||||
license status, type, email, and bundle version without leaking the raw key.
|
||||
|
||||
- Implement license save endpoint
|
||||
- Add AdminController::setLicense() and /api/admin/setLicense.php to accept a
|
||||
FRP1 license string via JSON, validate basic shape, and persist it to
|
||||
FR_PRO_LICENSE_FILE with strict 0600 permissions.
|
||||
- Return structured JSON success/error responses for the admin UI.
|
||||
|
||||
- Extend admin config model with branding + safer validation
|
||||
- Add `branding.customLogoUrl`, `branding.headerBgLight`, and
|
||||
`branding.headerBgDark` fields to AdminModel defaults and updateConfig().
|
||||
- Introduce AdminModel::sanitizeLogoUrl() to allow only site-relative /uploads
|
||||
paths or http(s) URLs; reject absolute filesystem paths, data: URLs, and
|
||||
javascript: URLs.
|
||||
- Continue to validate ONLYOFFICE docsOrigin as http(s) only, keeping core
|
||||
config hardening intact.
|
||||
|
||||
- New Pro-aware Admin Panel UI
|
||||
- Rework User Management section to group:
|
||||
- Add user / Remove user
|
||||
- Folder Access (per-folder ACL)
|
||||
- User Permissions (account-level flags)
|
||||
- Add Pro-only actions with clear gating:
|
||||
- “User groups” button (Pro)
|
||||
- “Client upload portal” button with “Pro · Coming soon” pill
|
||||
- Add “FileRise Pro” section:
|
||||
- Show current Pro status (Free vs Active) + license metadata.
|
||||
- Textarea for pasting license key, file upload helper, and “Save license”
|
||||
action wired to /api/admin/setLicense.php.
|
||||
- Optional “Copy current license” button when a license is present.
|
||||
- Add “Sponsor / Donations” section with fixed GitHub Sponsors and Ko-fi URLs
|
||||
and one-click copy/open buttons.
|
||||
|
||||
- Header branding controls (Pro)
|
||||
- Add Header Logo + Header Colors controls under Header Settings, gated by
|
||||
`config.pro.active`.
|
||||
- Allow uploading a logo via /api/pro/uploadBrandLogo.php and auto-filling the
|
||||
normalized /uploads path.
|
||||
- Add live-preview helpers to update the header logo and header background
|
||||
colors in the running UI after saving.
|
||||
|
||||
- Apply branding on app boot
|
||||
- Update main.js to read branding config on load and apply:
|
||||
- Custom header logo (or fallback to /assets/logo.svg).
|
||||
- Light/dark header background colors via CSS variables.
|
||||
- Keeps header consistent with saved branding across reloads and before
|
||||
opening the admin panel.
|
||||
|
||||
- Styling + UX polish
|
||||
- Add styles for new admin sections: collapsible headers, dark-mode aware
|
||||
modal content, and refined folder access grid.
|
||||
- Introduce .btn-pro-admin and .btn-pro-pill classes to render “Pro” and
|
||||
“Pro · Coming soon” pills overlayed on buttons, matching the existing
|
||||
header “Core/Pro” badge treatment.
|
||||
- Minor spacing/typography tweaks in admin panel and ACL UI.
|
||||
|
||||
Note: Core code remains MIT-licensed; Pro functionality is enabled via optional
|
||||
runtime hooks and separate closed-source bundle, without changing the core
|
||||
license text.
|
||||
|
||||
---
|
||||
|
||||
## Changes 11/14/2025 (v1.9.7)
|
||||
|
||||
release(v1.9.7): harden client path guard and refine header/folder strip CSS
|
||||
|
||||
@@ -238,4 +238,32 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
}
|
||||
|
||||
// Final: env var wins, else fallback
|
||||
define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare);
|
||||
define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare);
|
||||
|
||||
// --------------------------------
|
||||
// FileRise Pro (optional add-on)
|
||||
// --------------------------------
|
||||
|
||||
// Where the Pro license JSON lives
|
||||
if (!defined('PRO_LICENSE_FILE')) {
|
||||
define('PRO_LICENSE_FILE', PROJECT_ROOT . '/users/proLicense.json');
|
||||
}
|
||||
|
||||
// Inline/env license strings (optional)
|
||||
if (!defined('FR_PRO_LICENSE')) {
|
||||
define('FR_PRO_LICENSE', getenv('FR_PRO_LICENSE') ?: '');
|
||||
}
|
||||
if (!defined('FR_PRO_LICENSE_FILE')) {
|
||||
define('FR_PRO_LICENSE_FILE', getenv('FR_PRO_LICENSE_FILE') ?: '');
|
||||
}
|
||||
|
||||
// Optional Pro bootstrap (shipped only with Pro bundle)
|
||||
$proBootstrap = PROJECT_ROOT . '/src/pro/bootstrap_pro.php';
|
||||
if (is_file($proBootstrap)) {
|
||||
require_once $proBootstrap;
|
||||
}
|
||||
|
||||
// Safe default so the rest of the app always has the constant
|
||||
if (!defined('FR_PRO_ACTIVE')) {
|
||||
define('FR_PRO_ACTIVE', false);
|
||||
}
|
||||
|
||||
8
public/api/admin/setLicense.php
Normal file
8
public/api/admin/setLicense.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$ctrl = new AdminController();
|
||||
$ctrl->setLicense();
|
||||
@@ -67,17 +67,129 @@ body{letter-spacing: 0.2px;
|
||||
font-size: 34px !important;
|
||||
color: red !important;
|
||||
transform: translateY(-3px) !important;}
|
||||
.header-container{display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 55px;
|
||||
padding: 10px 20px;
|
||||
background-color: #2196F3;
|
||||
transition: background-color 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);}
|
||||
.dark-mode .header-container{background-color: #1f1f1f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);}
|
||||
.header-container{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 55px;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--header-bg-light, #2196F3);
|
||||
transition: background-color 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.dark-mode .header-container{
|
||||
background-color: var(--header-bg-dark, #1f1f1f);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.admin-core-badge,
|
||||
.admin-pro-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#adminPanelModal .section-content .pro-card {
|
||||
margin: 8px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#adminPanelModal .section-content {
|
||||
margin: 0px 10px;
|
||||
}
|
||||
.pro-license-meta {
|
||||
margin-top: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background-color: rgba(40, 167, 69, 0.08); /* light green tint */
|
||||
border: 1px solid rgba(40, 167, 69, 0.4);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.pro-license-meta div + div {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Dark mode tweak so it doesn't glow too bright */
|
||||
.dark-mode .pro-license-meta {
|
||||
background-color: rgba(40, 167, 69, 0.18);
|
||||
border-color: rgba(40, 167, 69, 0.6);
|
||||
color: #c8e6c9;
|
||||
}
|
||||
/* FileRise Pro button styling (admin) */
|
||||
.btn-pro-admin {
|
||||
background: linear-gradient(135deg, #ff9800, #ff5722);
|
||||
border-color: #ff9800;
|
||||
color: #1b0f00 !important;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 0 10px rgba(255, 152, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn-pro-admin:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
/* User management action bar */
|
||||
.admin-user-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.admin-user-actions .btn {
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.admin-user-actions .btn .material-icons {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ---------- Pro buttons + pill ---------- */
|
||||
.admin-user-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.btn-pro-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-pro-admin:disabled {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
|
||||
.btn-pro-pill {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: -4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: #ffc107;
|
||||
color: black;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
#userManagementContent {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
#darkModeIcon{color: #fff;}
|
||||
.header-logo{max-height: 50px;
|
||||
width: auto;
|
||||
|
||||
@@ -4,8 +4,46 @@ import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
|
||||
function normalizeLogoPath(raw) {
|
||||
if (!raw) return '';
|
||||
const parts = String(raw).split(':');
|
||||
let pic = parts[parts.length - 1];
|
||||
pic = pic.replace(/^:+/, '');
|
||||
if (pic && !pic.startsWith('/')) pic = '/' + pic;
|
||||
return pic;
|
||||
}
|
||||
|
||||
const version = window.APP_VERSION || "dev";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
function getAdminTitle(isPro, proVersion) {
|
||||
const corePill = `
|
||||
<span class="badge badge-pill badge-secondary admin-core-badge">
|
||||
Core ${version}
|
||||
</span>
|
||||
`;
|
||||
|
||||
if (!isPro) {
|
||||
// Free/core only
|
||||
return `
|
||||
${t("admin_panel")}
|
||||
${corePill}
|
||||
`;
|
||||
}
|
||||
|
||||
const pv = proVersion ? `Pro v${proVersion}` : 'Pro';
|
||||
|
||||
const proPill = `
|
||||
<span class="badge badge-pill badge-warning admin-pro-badge">
|
||||
${pv}
|
||||
</span>
|
||||
`;
|
||||
|
||||
return `
|
||||
${t("admin_panel")}
|
||||
${corePill}
|
||||
${proPill}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function buildFullGrantsForAllFolders(folders) {
|
||||
@@ -16,6 +54,49 @@ function buildFullGrantsForAllFolders(folders) {
|
||||
};
|
||||
return folders.reduce((acc, f) => { acc[f] = { ...allTrue }; return acc; }, {});
|
||||
}
|
||||
function applyHeaderColorsFromAdmin() {
|
||||
try {
|
||||
const lightInput = document.getElementById('brandingHeaderBgLight');
|
||||
const darkInput = document.getElementById('brandingHeaderBgDark');
|
||||
const root = document.documentElement;
|
||||
|
||||
const light = lightInput ? lightInput.value.trim() : '';
|
||||
const dark = darkInput ? darkInput.value.trim() : '';
|
||||
|
||||
if (light) root.style.setProperty('--header-bg-light', light);
|
||||
else root.style.removeProperty('--header-bg-light');
|
||||
|
||||
if (dark) root.style.setProperty('--header-bg-dark', dark);
|
||||
else root.style.removeProperty('--header-bg-dark');
|
||||
} catch (e) {
|
||||
console.warn('Failed to live-update header colors from admin panel', e);
|
||||
}
|
||||
}
|
||||
function updateHeaderLogoFromAdmin() {
|
||||
try {
|
||||
const input = document.getElementById('brandingCustomLogoUrl');
|
||||
const logoImg = document.querySelector('.header-logo img');
|
||||
if (!logoImg) return;
|
||||
|
||||
let url = (input && input.value.trim()) || '';
|
||||
|
||||
// If they used a bare "uploads/..." path, normalize to "/uploads/..."
|
||||
if (url && !url.startsWith('/') && url.startsWith('uploads/')) {
|
||||
url = '/' + url;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
logoImg.setAttribute('src', url);
|
||||
logoImg.setAttribute('alt', 'Site logo');
|
||||
} else {
|
||||
// fall back to default FileRise logo
|
||||
logoImg.setAttribute('src', '/assets/logo.svg?v={{APP_QVER}}');
|
||||
logoImg.setAttribute('alt', 'FileRise');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to live-update header logo from admin panel', e);
|
||||
}
|
||||
}
|
||||
|
||||
/* === BEGIN: Folder Access helpers (merged + improved) === */
|
||||
function qs(scope, sel) { return (scope || document).querySelector(sel); }
|
||||
@@ -175,7 +256,7 @@ async function safeJson(res) {
|
||||
.dark-mode .form-control::placeholder { color:#888; }
|
||||
|
||||
.section-header {
|
||||
background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:4px; font-weight:bold;
|
||||
background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:12px; font-weight:bold;
|
||||
display:flex; align-items:center; justify-content:space-between; margin-top:16px;
|
||||
}
|
||||
.section-header:first-of-type { margin-top:0; }
|
||||
@@ -301,7 +382,10 @@ function captureInitialAdminConfig() {
|
||||
disableOIDCLogin: !!document.getElementById("disableOIDCLogin")?.checked,
|
||||
enableWebDAV: !!document.getElementById("enableWebDAV")?.checked,
|
||||
sharedMaxUploadSize: (document.getElementById("sharedMaxUploadSize")?.value || "").trim(),
|
||||
globalOtpauthUrl: (document.getElementById("globalOtpauthUrl")?.value || "").trim()
|
||||
globalOtpauthUrl: (document.getElementById("globalOtpauthUrl")?.value || "").trim(),
|
||||
brandingCustomLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
||||
brandingHeaderBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
||||
brandingHeaderBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
||||
};
|
||||
}
|
||||
function hasUnsavedChanges() {
|
||||
@@ -319,7 +403,10 @@ function hasUnsavedChanges() {
|
||||
getChk("disableOIDCLogin") !== o.disableOIDCLogin ||
|
||||
getChk("enableWebDAV") !== o.enableWebDAV ||
|
||||
getVal("sharedMaxUploadSize") !== o.sharedMaxUploadSize ||
|
||||
getVal("globalOtpauthUrl") !== o.globalOtpauthUrl
|
||||
getVal("globalOtpauthUrl") !== o.globalOtpauthUrl ||
|
||||
getVal("brandingCustomLogoUrl") !== (o.brandingCustomLogoUrl || "") ||
|
||||
getVal("brandingHeaderBgLight") !== (o.brandingHeaderBgLight || "") ||
|
||||
getVal("brandingHeaderBgDark") !== (o.brandingHeaderBgDark || "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -460,6 +547,16 @@ export function openAdminPanel() {
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
|
||||
const dark = document.body.classList.contains("dark-mode");
|
||||
const proInfo = config.pro || {};
|
||||
const isPro = !!proInfo.active;
|
||||
const proType = proInfo.type || '';
|
||||
const proEmail = proInfo.email || '';
|
||||
const proVersion = proInfo.version || 'not installed';
|
||||
const proLicense = proInfo.license || '';
|
||||
const brandingCfg = config.branding || {};
|
||||
const brandingCustomLogoUrl = brandingCfg.customLogoUrl || "";
|
||||
const brandingHeaderBgLight = brandingCfg.headerBgLight || "";
|
||||
const brandingHeaderBgDark = brandingCfg.headerBgDark || "";
|
||||
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const inner = `
|
||||
background:${dark ? "#2c2c2c" : "#fff"};
|
||||
@@ -484,7 +581,7 @@ export function openAdminPanel() {
|
||||
mdl.innerHTML = `
|
||||
<div class="modal-content" style="${inner}">
|
||||
<div class="editor-close-btn" id="closeAdminPanel">×</div>
|
||||
<h3>${adminTitle}</h3>
|
||||
<h3>${getAdminTitle(isPro, proVersion)}</h3>
|
||||
<form id="adminPanelForm">
|
||||
${[
|
||||
{ id: "userManagement", label: t("user_management") },
|
||||
@@ -495,6 +592,7 @@ export function openAdminPanel() {
|
||||
{ id: "upload", label: t("shared_max_upload_size_bytes_title") },
|
||||
{ id: "oidc", label: t("oidc_configuration") + " & TOTP" },
|
||||
{ id: "shareLinks", label: t("manage_shared_links") },
|
||||
{ id: "pro", label: "FileRise Pro" },
|
||||
{ id: "sponsor", label: (typeof tf === 'function' ? tf("sponsor_donations", "Sponsor / Donations") : "Sponsor / Donations") }
|
||||
].map(sec => `
|
||||
<div id="${sec.id}Header" class="section-header collapsed">
|
||||
@@ -515,18 +613,100 @@ export function openAdminPanel() {
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
|
||||
["userManagement", "headerSettings", "loginOptions", "webdav", "onlyoffice", "upload", "oidc", "shareLinks", "sponsor"]
|
||||
["userManagement", "headerSettings", "loginOptions", "webdav", "onlyoffice", "upload", "oidc", "shareLinks", "pro", "sponsor"]
|
||||
.forEach(id => {
|
||||
document.getElementById(id + "Header")
|
||||
.addEventListener("click", () => toggleSection(id));
|
||||
});
|
||||
|
||||
document.getElementById("userManagementContent").innerHTML = `
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success me-2">${t("add_user")}</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger me-2">${t("remove_user")}</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${tf("folder_access", "Folder Access")}</button>
|
||||
<button type="button" id="adminOpenUserFlags" class="btn btn-secondary">${tf("user_permissions", "User Permissions")}</button>
|
||||
`;
|
||||
document.getElementById("userManagementContent").innerHTML = `
|
||||
<div class="admin-user-actions">
|
||||
<!-- Core buttons -->
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success btn-sm">
|
||||
<i class="material-icons">person_add</i>
|
||||
<span>${t("add_user")}</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger btn-sm">
|
||||
<i class="material-icons">person_remove</i>
|
||||
<span>${t("remove_user")}</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary btn-sm">
|
||||
<i class="material-icons">folder_shared</i>
|
||||
<span>${tf("folder_access", "Folder Access")}</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id="adminOpenUserFlags" class="btn btn-secondary btn-sm">
|
||||
<i class="material-icons">tune</i>
|
||||
<span>${tf("user_permissions", "User Permissions")}</span>
|
||||
</button>
|
||||
|
||||
<!-- Pro-only: User groups -->
|
||||
${
|
||||
isPro
|
||||
? `
|
||||
<div class="btn-pro-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
id="adminOpenUserGroups"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
<i class="material-icons">groups</i>
|
||||
<span>User groups</span>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="btn-pro-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
id="adminOpenUserGroups"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
<i class="material-icons">groups</i>
|
||||
<span>User groups</span>
|
||||
</button>
|
||||
<span class="btn-pro-pill">Pro · Coming soon</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<!-- Pro roadmap: Client portal -->
|
||||
${
|
||||
isPro
|
||||
? `
|
||||
<div class="btn-pro-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
id="adminOpenClientPortal"
|
||||
class="btn btn-sm btn-pro-admin"
|
||||
title="Client upload portals are part of FileRise Pro.">
|
||||
<i class="material-icons">cloud_upload</i>
|
||||
<span>Client upload portal</span>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="btn-pro-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
id="adminOpenClientPortal"
|
||||
class="btn btn-sm btn-pro-admin"
|
||||
disabled
|
||||
title="Planned FileRise Pro feature: client upload portals">
|
||||
<i class="material-icons">cloud_upload</i>
|
||||
<span>Client upload portal</span>
|
||||
</button>
|
||||
<span class="btn-pro-pill">Pro · Coming soon</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<small class="text-muted d-block" style="margin-top:6px;">
|
||||
Use the core tools to manage users and per-folder access.
|
||||
User groups and Client upload portals are planned FileRise Pro features.
|
||||
</small>
|
||||
`;
|
||||
|
||||
document.getElementById("adminOpenAddUser")
|
||||
.addEventListener("click", () => {
|
||||
@@ -541,13 +721,174 @@ export function openAdminPanel() {
|
||||
document.getElementById("adminOpenUserPermissions")
|
||||
.addEventListener("click", openUserPermissionsModal);
|
||||
|
||||
document.getElementById("headerSettingsContent").innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="headerTitle">${t("header_title_text")}:</label>
|
||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle || ""}" />
|
||||
</div>
|
||||
`;
|
||||
wireHeaderTitleLive();
|
||||
// Pro-only stubs for future features
|
||||
const regBtn = document.getElementById("adminOpenUserRegistration");
|
||||
const groupsBtn = document.getElementById("adminOpenUserGroups");
|
||||
const clientBtn = document.getElementById("adminOpenClientPortal");
|
||||
|
||||
if (regBtn) {
|
||||
regBtn.addEventListener("click", () => {
|
||||
if (!isPro) {
|
||||
showToast("User registration is a FileRise Pro feature. Visit filerise.net to purchase a license.");
|
||||
window.open("https://filerise.net", "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
// Placeholder for future Pro UI:
|
||||
showToast("User registration management is coming soon in FileRise Pro.");
|
||||
});
|
||||
}
|
||||
|
||||
if (groupsBtn) {
|
||||
groupsBtn.addEventListener("click", () => {
|
||||
if (!isPro) {
|
||||
showToast("User groups are a FileRise Pro feature. Visit filerise.net to purchase a license.");
|
||||
window.open("https://filerise.net", "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
// Placeholder for future Pro UI:
|
||||
showToast("User groups management is coming soon in FileRise Pro.");
|
||||
});
|
||||
}
|
||||
|
||||
if (clientBtn) {
|
||||
clientBtn.addEventListener("click", () => {
|
||||
if (!isPro) {
|
||||
showToast("Client portal uploads are a FileRise Pro feature. Visit filerise.net to purchase a license.");
|
||||
window.open("https://filerise.net", "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
// Placeholder for future Pro UI:
|
||||
showToast("Client portal uploads are coming soon in FileRise Pro.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("headerSettingsContent").innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="headerTitle">${t("header_title_text")}:</label>
|
||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle || ""}" />
|
||||
</div>
|
||||
|
||||
<!-- Pro: Logo -->
|
||||
<div class="form-group" style="margin-top:16px;">
|
||||
<label for="brandingCustomLogoUrl">
|
||||
Header Logo
|
||||
${!isPro ? '<span class="badge badge-pill badge-warning admin-pro-badge" style="margin-left:6px;">Pro</span>' : ''}
|
||||
</label>
|
||||
<small class="text-muted d-block mb-1">
|
||||
${isPro
|
||||
? 'Upload a logo image or paste a local path.'
|
||||
: 'Requires FileRise Pro to enable custom header branding.'}
|
||||
</small>
|
||||
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="brandingCustomLogoUrl"
|
||||
class="form-control"
|
||||
placeholder="/uploads/profile_pics/logo.png"
|
||||
value="${isPro ? (brandingCustomLogoUrl.replace(/"/g, '"')) : ''}"
|
||||
${!isPro ? 'disabled data-disabled-reason="pro"' : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="file"
|
||||
id="brandingLogoFile"
|
||||
class="form-control"
|
||||
accept="image/*"
|
||||
${!isPro ? 'disabled' : ''}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary"
|
||||
id="brandingUploadBtn"
|
||||
${!isPro ? 'disabled' : ''}>
|
||||
Upload logo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pro: Header colors -->
|
||||
<div class="form-group" style="margin-top:16px;">
|
||||
<label>
|
||||
Header Colors
|
||||
${!isPro ? '<span class="badge badge-pill badge-warning admin-pro-badge" style="margin-left:6px;">Pro</span>' : ''}
|
||||
</label>
|
||||
<div class="d-flex align-items-center" style="gap: 12px; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="brandingHeaderBgLight" class="d-block" style="font-size: 12px; margin-bottom: 4px;">Light mode</label>
|
||||
<input
|
||||
type="color"
|
||||
id="brandingHeaderBgLight"
|
||||
value="${brandingHeaderBgLight || '#2196F3'}"
|
||||
${!isPro ? 'disabled' : ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brandingHeaderBgDark" class="d-block" style="font-size: 12px; margin-bottom: 4px;">Dark mode</label>
|
||||
<input
|
||||
type="color"
|
||||
id="brandingHeaderBgDark"
|
||||
value="${brandingHeaderBgDark || '#1f1f1f'}"
|
||||
${!isPro ? 'disabled' : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">
|
||||
${isPro
|
||||
? 'If left empty, FileRise uses its default blue and dark header colors.'
|
||||
: 'Requires FileRise Pro to enable custom color branding.'}
|
||||
|
||||
</small>
|
||||
</div>
|
||||
`;
|
||||
wireHeaderTitleLive();
|
||||
|
||||
// Upload logo -> reuse profile picture endpoint, then fill the logo path
|
||||
if (isPro) {
|
||||
const fileInput = document.getElementById('brandingLogoFile');
|
||||
const uploadBtn = document.getElementById('brandingUploadBtn');
|
||||
const urlInput = document.getElementById('brandingCustomLogoUrl');
|
||||
|
||||
if (fileInput && uploadBtn && urlInput) {
|
||||
uploadBtn.addEventListener('click', async () => {
|
||||
const f = fileInput.files && fileInput.files[0];
|
||||
if (!f) {
|
||||
showToast('Please choose an image first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('brand_logo', f); // <- must match PHP field
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/pro/uploadBrandLogo.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||
body: fd
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let js = {};
|
||||
try { js = JSON.parse(text || '{}'); } catch (e) { js = {}; }
|
||||
|
||||
if (!res.ok || !js.url) {
|
||||
showToast(js.error || 'Error uploading logo');
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeLogoPath(js.url); // your helper
|
||||
urlInput.value = normalized;
|
||||
showToast('Logo uploaded. Don\'t forget to Save settings.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Error uploading logo');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("loginOptionsContent").innerHTML = `
|
||||
<div class="form-group"><input type="checkbox" id="disableFormLogin" /> <label for="disableFormLogin">${t("disable_login_form")}</label></div>
|
||||
@@ -912,6 +1253,236 @@ async function ooProbeFrame(docsOrigin, timeoutMs = 4000) {
|
||||
|
||||
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
|
||||
|
||||
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
|
||||
|
||||
// --- FileRise Pro / License section ---
|
||||
const proContent = document.getElementById("proContent");
|
||||
if (proContent) {
|
||||
const proMetaHtml =
|
||||
isPro && (proType || proEmail || proVersion)
|
||||
? `
|
||||
<div class="pro-license-meta">
|
||||
<div>
|
||||
✅ ${proType ? `License type: ${proType}` : 'License active'}
|
||||
${proType && proEmail ? ' • ' : ''}
|
||||
${proEmail ? `Licensed to: ${proEmail}` : ''}
|
||||
</div>
|
||||
<div>
|
||||
Pro bundle version: v${proVersion}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
proContent.innerHTML = `
|
||||
<div class="card pro-card" style="padding:12px; border:1px solid #ddd; border-radius:12px; max-width:620px; margin:8px auto;">
|
||||
<div>
|
||||
<!-- Title row with pill aligned to "FileRise Pro" -->
|
||||
<div class="d-flex align-items-center" style="gap:8px;">
|
||||
<strong>FileRise Pro</strong>
|
||||
<span class="badge badge-pill ${isPro ? 'badge-success' : 'badge-secondary'} admin-pro-badge">
|
||||
${isPro ? 'Active' : 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Subtitle + meta under the title -->
|
||||
<div style="font-size:12px; color:#777; margin-top:2px;">
|
||||
${isPro
|
||||
? 'Pro features are currently enabled on this instance.'
|
||||
: 'You are running the free edition. Enter a license key to activate FileRise Pro.'}
|
||||
</div>
|
||||
${proMetaHtml}
|
||||
</div>
|
||||
|
||||
${isPro ? `
|
||||
<div style="margin-top:8px;">
|
||||
<a
|
||||
href="https://filerise.net/pro/update.php"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
Download latest Pro bundle
|
||||
</a>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
Opens filerise.net in a new tab where you can enter your Pro license
|
||||
to download the latest FileRise Pro ZIP.
|
||||
</small>
|
||||
</div>
|
||||
` : `
|
||||
<div style="margin-top:8px;">
|
||||
<a
|
||||
href="https://filerise.net/pro/checkout.php"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
Buy FileRise Pro
|
||||
</a>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
Opens filerise.net in a new tab so you can purchase a FileRise Pro license.
|
||||
</small>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="form-group" style="margin-top:10px;">
|
||||
<label for="proLicenseInput" style="font-size:12px;">License key</label>
|
||||
<textarea
|
||||
id="proLicenseInput"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Paste your FileRise Pro license key here..."></textarea>
|
||||
<small class="text-muted">
|
||||
You can purchase a license at
|
||||
<a href="https://filerise.net" target="_blank" rel="noopener noreferrer">filerise.net</a>.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
${isPro && proLicense ? `
|
||||
<div style="margin-top:6px;">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="proCopyLicenseBtn">
|
||||
Copy current license
|
||||
</button>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
Copies the saved license so you can reuse it for upgrades or downloads on filerise.net.
|
||||
</small>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="form-group" style="margin-top:6px;">
|
||||
<label style="font-size:12px;">Or upload license file</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="file"
|
||||
id="proLicenseFile"
|
||||
class="form-control"
|
||||
accept=".lic,.json,.txt,.filerise-lic"
|
||||
/>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="proLoadLicenseFileBtn">
|
||||
Load from file
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
Supported: FileRise.lic, plain text with FRP1... or JSON containing a <code>license</code> field.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary btn-sm" id="proSaveLicenseBtn" style="margin-top:8px;">
|
||||
Save license
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Pre-fill textarea with saved license if present
|
||||
const licenseTextarea = document.getElementById('proLicenseInput');
|
||||
if (licenseTextarea && proLicense) {
|
||||
licenseTextarea.value = proLicense;
|
||||
}
|
||||
|
||||
// File upload → fill textarea (unchanged)
|
||||
const fileInput = document.getElementById('proLicenseFile');
|
||||
const fileBtn = document.getElementById('proLoadLicenseFileBtn');
|
||||
|
||||
if (fileInput && fileBtn && licenseTextarea) {
|
||||
fileBtn.addEventListener('click', () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) {
|
||||
showToast('Please choose a license file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let raw = String(e.target.result || '').trim();
|
||||
let license = raw;
|
||||
|
||||
try {
|
||||
const js = JSON.parse(raw);
|
||||
if (js && typeof js.license === 'string') {
|
||||
license = js.license.trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// not JSON, treat as plain text
|
||||
}
|
||||
|
||||
if (!license || !license.startsWith('FRP1.')) {
|
||||
showToast('Could not find a valid FRP1 license in that file.');
|
||||
return;
|
||||
}
|
||||
|
||||
licenseTextarea.value = license;
|
||||
showToast('License loaded from file. Click "Save license" to apply.');
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
showToast('Error reading license file.');
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Copy current license button
|
||||
const proCopyBtn = document.getElementById('proCopyLicenseBtn');
|
||||
if (proCopyBtn && proLicense) {
|
||||
proCopyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(proLicense);
|
||||
} else {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = proLicense;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
ta.remove();
|
||||
}
|
||||
showToast('License copied to clipboard.');
|
||||
} catch {
|
||||
showToast('Could not copy license. Please copy it manually.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save license handler (unchanged)
|
||||
const proSaveBtn = document.getElementById('proSaveLicenseBtn');
|
||||
if (proSaveBtn) {
|
||||
proSaveBtn.addEventListener('click', async () => {
|
||||
const ta = document.getElementById('proLicenseInput');
|
||||
const license = (ta && ta.value.trim()) || '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/setLicense.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]')?.content || '')
|
||||
},
|
||||
body: JSON.stringify({ license }),
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let data = {};
|
||||
try { data = JSON.parse(text || '{}'); } catch (e) { data = {}; }
|
||||
|
||||
if (!res.ok || !data.success) {
|
||||
console.error('setLicense error:', res.status, text);
|
||||
showToast(data.error || 'Error saving license');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('License saved. Reloading…');
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Error saving license');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- end FileRise Pro section ---
|
||||
|
||||
document.getElementById("saveAdminSettings")
|
||||
.addEventListener("click", handleSave);
|
||||
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
|
||||
@@ -1065,6 +1636,11 @@ function handleSave() {
|
||||
// clientId/clientSecret: only include when replacing
|
||||
},
|
||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim(),
|
||||
branding: {
|
||||
customLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
||||
headerBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
||||
headerBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
||||
},
|
||||
};
|
||||
|
||||
const idEl = document.getElementById("oidcClientId");
|
||||
@@ -1119,6 +1695,8 @@ function handleSave() {
|
||||
if (j.error) { showToast('Error: ' + j.error); return; }
|
||||
showToast('Settings saved.');
|
||||
closeAdminPanel();
|
||||
applyHeaderColorsFromAdmin();
|
||||
updateHeaderLogoFromAdmin();
|
||||
})
|
||||
.catch(() => showToast('Save failed.'));
|
||||
}
|
||||
|
||||
@@ -406,6 +406,40 @@ function bindDarkMode() {
|
||||
|
||||
// Always keep <title> correct early (no visual flicker)
|
||||
document.title = title;
|
||||
// --- Header logo (branding) in BOTH phases ---
|
||||
try {
|
||||
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
||||
const customLogoUrl = branding.customLogoUrl || "";
|
||||
const logoImg = document.querySelector('.header-logo img');
|
||||
if (logoImg) {
|
||||
if (customLogoUrl) {
|
||||
logoImg.setAttribute('src', customLogoUrl);
|
||||
logoImg.setAttribute('alt', 'Site logo');
|
||||
} else {
|
||||
// fall back to default FileRise logo
|
||||
logoImg.setAttribute('src', '/assets/logo.svg?v={{APP_QVER}}');
|
||||
logoImg.setAttribute('alt', 'FileRise');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// non-fatal; ignore branding issues
|
||||
}
|
||||
// --- Header colors (branding) in BOTH phases ---
|
||||
try {
|
||||
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
||||
const root = document.documentElement;
|
||||
|
||||
const light = branding.headerBgLight || '';
|
||||
const dark = branding.headerBgDark || '';
|
||||
|
||||
if (light) root.style.setProperty('--header-bg-light', light);
|
||||
else root.style.removeProperty('--header-bg-light');
|
||||
|
||||
if (dark) root.style.setProperty('--header-bg-dark', dark);
|
||||
else root.style.removeProperty('--header-bg-dark');
|
||||
} catch (e) {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
// --- Login options (apply in BOTH phases so login page is correct) ---
|
||||
const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
|
||||
|
||||
@@ -6,91 +6,271 @@ require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
|
||||
class AdminController
|
||||
{
|
||||
public function getConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$config = AdminModel::getConfig();
|
||||
if (isset($config['error'])) {
|
||||
http_response_code(500);
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode(['error' => $config['error']], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
/** Enforce authentication (401). */
|
||||
private static function requireAuth(): void
|
||||
{
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/** Enforce admin (401). */
|
||||
private static function requireAdmin(): void
|
||||
{
|
||||
self::requireAuth();
|
||||
|
||||
// Prefer the session flag
|
||||
$isAdmin = (!empty($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true);
|
||||
|
||||
// Fallback: check the user’s role in storage (e.g., users.txt/DB)
|
||||
if (!$isAdmin) {
|
||||
$u = $_SESSION['username'] ?? '';
|
||||
if ($u) {
|
||||
try {
|
||||
// UserModel::getUserRole($u) should return '1' for admins
|
||||
$isAdmin = (UserModel::getUserRole($u) === '1');
|
||||
if ($isAdmin) {
|
||||
// Normalize session so downstream ACL checks see admin
|
||||
$_SESSION['isAdmin'] = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore and continue to deny
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isAdmin) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Admin privileges required.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
/** Get headers in lowercase, robust across SAPIs. */
|
||||
private static function headersLower(): array
|
||||
{
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
$out = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
$out[strtolower($k)] = $v;
|
||||
}
|
||||
// Fallbacks from $_SERVER if needed
|
||||
foreach ($_SERVER as $k => $v) {
|
||||
if (strpos($k, 'HTTP_') === 0) {
|
||||
$h = strtolower(str_replace('_', '-', substr($k, 5)));
|
||||
if (!isset($out[$h])) $out[$h] = $v;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Enforce CSRF using X-CSRF-Token header (or csrfToken param as fallback). */
|
||||
private static function requireCsrf(): void
|
||||
{
|
||||
$h = self::headersLower();
|
||||
$token = trim($h['x-csrf-token'] ?? ($_POST['csrfToken'] ?? ''));
|
||||
if (empty($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Invalid CSRF token']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/** Read JSON body (empty array if not valid). */
|
||||
private static function readJson(): array
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
public function getConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$config = AdminModel::getConfig();
|
||||
if (isset($config['error'])) {
|
||||
http_response_code(500);
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode(['error' => $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'] ?? '');
|
||||
|
||||
// ---- Pro / license info (all guarded for clean core installs) ----
|
||||
$licenseString = null;
|
||||
if (defined('PRO_LICENSE_FILE') && PRO_LICENSE_FILE && @is_file(PRO_LICENSE_FILE)) {
|
||||
$json = @file_get_contents(PRO_LICENSE_FILE);
|
||||
if ($json !== false) {
|
||||
$decoded = json_decode($json, true);
|
||||
if (is_array($decoded) && !empty($decoded['license'])) {
|
||||
$licenseString = (string) $decoded['license'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$proActive = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
|
||||
|
||||
// FR_PRO_INFO is only defined when bootstrap_pro.php has run; guard it
|
||||
$proPayload = [];
|
||||
if (defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)) {
|
||||
$p = FR_PRO_INFO['payload'] ?? null;
|
||||
if (is_array($p)) {
|
||||
$proPayload = $p;
|
||||
}
|
||||
}
|
||||
|
||||
$proType = $proPayload['type'] ?? null;
|
||||
$proEmail = $proPayload['email'] ?? null;
|
||||
$proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
|
||||
|
||||
// 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,
|
||||
],
|
||||
'branding' => [
|
||||
'customLogoUrl' => (string)($config['branding']['customLogoUrl'] ?? ''),
|
||||
'headerBgLight' => (string)($config['branding']['headerBgLight'] ?? ''),
|
||||
'headerBgDark' => (string)($config['branding']['headerBgDark'] ?? ''),
|
||||
],
|
||||
'pro' => [
|
||||
'active' => $proActive,
|
||||
'type' => $proType,
|
||||
'email' => $proEmail,
|
||||
'version' => $proVersion,
|
||||
'license' => $licenseString,
|
||||
],
|
||||
];
|
||||
|
||||
$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')
|
||||
|| defined('ONLYOFFICE_PUBLIC_ORIGIN')
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
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 setLicense(): void
|
||||
{
|
||||
// Always respond JSON
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
// Same guards as other admin endpoints
|
||||
self::requireAuth();
|
||||
self::requireAdmin();
|
||||
self::requireCsrf();
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw ?: '{}', true);
|
||||
if (!is_array($data)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid JSON body']);
|
||||
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);
|
||||
$license = isset($data['license']) ? trim((string)$data['license']) : '';
|
||||
|
||||
$effDocs = defined('ONLYOFFICE_DOCS_ORIGIN') && ONLYOFFICE_DOCS_ORIGIN !== ''
|
||||
? (string) ONLYOFFICE_DOCS_ORIGIN
|
||||
: (string) ($ooCfg['docsOrigin'] ?? '');
|
||||
// Store license + updatedAt in JSON file
|
||||
if (!defined('PRO_LICENSE_FILE')) {
|
||||
// Fallback if constant not defined for some reason
|
||||
define('PRO_LICENSE_FILE', PROJECT_ROOT . '/users/proLicense.json');
|
||||
}
|
||||
|
||||
$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,
|
||||
],
|
||||
$payload = [
|
||||
'license' => $license,
|
||||
'updatedAt' => gmdate('c'),
|
||||
];
|
||||
|
||||
$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);
|
||||
$dir = dirname(PRO_LICENSE_FILE);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to create license dir']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-admins / unauthenticated: only the public subset
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode($public, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
return;
|
||||
$json = json_encode($payload, JSON_PRETTY_PRINT);
|
||||
if ($json === false || file_put_contents(PRO_LICENSE_FILE, $json) === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to write license file']);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Exception: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateConfig(): void
|
||||
{
|
||||
@@ -149,6 +329,11 @@ class AdminController
|
||||
'clientSecret'=> '',
|
||||
'redirectUri' => ''
|
||||
],
|
||||
'branding' => [
|
||||
'customLogoUrl' => '',
|
||||
'headerBgLight' => '',
|
||||
'headerBgDark' => '',
|
||||
],
|
||||
];
|
||||
|
||||
// header_title (cap length and strip control chars)
|
||||
@@ -250,6 +435,7 @@ class AdminController
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// —– persist merged config —–
|
||||
// ---- ONLYOFFICE: merge from payload (unless locked by PHP defines) ----
|
||||
@@ -286,6 +472,22 @@ class AdminController
|
||||
|
||||
$merged['onlyoffice'] = $oo;
|
||||
}
|
||||
// Branding: pass through raw strings; AdminModel enforces Pro + sanitization.
|
||||
if (isset($data['branding']) && is_array($data['branding'])) {
|
||||
if (!isset($merged['branding']) || !is_array($merged['branding'])) {
|
||||
$merged['branding'] = [
|
||||
'customLogoUrl' => '',
|
||||
'headerBgLight' => '',
|
||||
'headerBgDark' => '',
|
||||
];
|
||||
}
|
||||
foreach (['customLogoUrl', 'headerBgLight', 'headerBgDark'] as $key) {
|
||||
if (array_key_exists($key, $data['branding'])) {
|
||||
$merged['branding'][$key] = (string)$data['branding'][$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = AdminModel::updateConfig($merged);
|
||||
if (isset($result['error'])) {
|
||||
http_response_code(500);
|
||||
|
||||
@@ -649,8 +649,16 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Assuming /uploads maps to UPLOAD_DIR publicly
|
||||
$url = '/uploads/profile_pics/' . $filename;
|
||||
$fsPath = rtrim(UPLOAD_DIR, '/\\') . '/profile_pics/' . $filename;
|
||||
|
||||
// Remove the filesystem root (PROJECT_ROOT) so we get a web-relative path
|
||||
$root = rtrim(PROJECT_ROOT, '/\\');
|
||||
$url = preg_replace('#^' . preg_quote($root, '#') . '#', '', $fsPath);
|
||||
|
||||
// Ensure it starts with /
|
||||
if ($url === '' || $url[0] !== '/') {
|
||||
$url = '/' . $url;
|
||||
}
|
||||
|
||||
$result = UserModel::setProfilePicture($_SESSION['username'], $url);
|
||||
if (!($result['success'] ?? false)) {
|
||||
@@ -667,6 +675,76 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload branding logo (Pro-only; admin, CSRF).
|
||||
* Reuses the profile_pics directory but does NOT change the user's avatar.
|
||||
*/
|
||||
public function uploadBrandLogo()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
|
||||
// Auth, admin & CSRF
|
||||
self::requireAuth();
|
||||
self::requireAdmin();
|
||||
self::requireCsrf();
|
||||
|
||||
if (empty($_FILES['brand_logo']) || $_FILES['brand_logo']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'No file uploaded or error']);
|
||||
exit;
|
||||
}
|
||||
$file = $_FILES['brand_logo'];
|
||||
|
||||
// Validate MIME & size (same rules as uploadPicture)
|
||||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
if (!isset($allowed[$mime])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid file type']);
|
||||
exit;
|
||||
}
|
||||
if ($file['size'] > 2 * 1024 * 1024) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'File too large']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Destination: reuse profile_pics directory
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . '/profile_pics';
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Cannot create upload folder']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ext = $allowed[$mime];
|
||||
$user = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_SESSION['username'] ?? 'logo');
|
||||
$filename = 'branding_' . $user . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
$dest = $uploadDir . '/' . $filename;
|
||||
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to save file']);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$fsPath = rtrim(UPLOAD_DIR, '/\\') . '/profile_pics/' . $filename;
|
||||
|
||||
// Remove the filesystem root (PROJECT_ROOT) so we get a web-relative path
|
||||
$root = rtrim(PROJECT_ROOT, '/\\');
|
||||
$url = preg_replace('#^' . preg_quote($root, '#') . '#', '', $fsPath);
|
||||
|
||||
// Ensure it starts with /
|
||||
if ($url === '' || $url[0] !== '/') {
|
||||
$url = '/' . $url;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'url' => $url]);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function siteConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -73,6 +73,27 @@ class AdminModel
|
||||
return ($scheme === 'http' || $scheme === 'https') ? $url : '';
|
||||
}
|
||||
|
||||
/** Allow logo URLs that are either site-relative (/uploads/…) or http(s). */
|
||||
private static function sanitizeLogoUrl($url): string
|
||||
{
|
||||
$url = trim((string)$url);
|
||||
if ($url === '') return '';
|
||||
|
||||
// 1) Site-relative like "/uploads/profile_pics/branding_foo.png"
|
||||
if ($url[0] === '/') {
|
||||
// Strip CRLF just in case
|
||||
$url = preg_replace('~[\r\n]+~', '', $url);
|
||||
// Don’t allow sneaky schemes embedded in a relative path
|
||||
if (strpos($url, '://') !== false) {
|
||||
return '';
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 2) Fallback to plain http(s) validation
|
||||
return self::sanitizeHttpUrl($url);
|
||||
}
|
||||
|
||||
public static function buildPublicSubset(array $config): array
|
||||
{
|
||||
$public = [
|
||||
@@ -89,6 +110,17 @@ class AdminModel
|
||||
'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
|
||||
'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
|
||||
],
|
||||
'branding' => [
|
||||
'customLogoUrl' => self::sanitizeLogoUrl(
|
||||
$config['branding']['customLogoUrl'] ?? ''
|
||||
),
|
||||
'headerBgLight' => self::sanitizeColorHex(
|
||||
$config['branding']['headerBgLight'] ?? ''
|
||||
),
|
||||
'headerBgDark' => self::sanitizeColorHex(
|
||||
$config['branding']['headerBgDark'] ?? ''
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
// NEW: include ONLYOFFICE minimal public flag
|
||||
@@ -226,6 +258,30 @@ $public['onlyoffice'] = ['enabled' => $ooEnabled];
|
||||
|
||||
$configUpdate['onlyoffice'] = $norm;
|
||||
}
|
||||
|
||||
// Branding (Pro-only). Normalize and only persist when Pro is active.
|
||||
if (!isset($configUpdate['branding']) || !is_array($configUpdate['branding'])) {
|
||||
$configUpdate['branding'] = [
|
||||
'customLogoUrl' => '',
|
||||
'headerBgLight' => '',
|
||||
'headerBgDark' => '',
|
||||
];
|
||||
} else {
|
||||
$logo = self::sanitizeLogoUrl($configUpdate['branding']['customLogoUrl'] ?? '');
|
||||
$light = self::sanitizeColorHex($configUpdate['branding']['headerBgLight'] ?? '');
|
||||
$dark = self::sanitizeColorHex($configUpdate['branding']['headerBgDark'] ?? '');
|
||||
|
||||
if (defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE) {
|
||||
$configUpdate['branding']['customLogoUrl'] = $logo;
|
||||
$configUpdate['branding']['headerBgLight'] = $light;
|
||||
$configUpdate['branding']['headerBgDark'] = $dark;
|
||||
} else {
|
||||
// Free mode: always clear branding customizations
|
||||
$configUpdate['branding']['customLogoUrl'] = '';
|
||||
$configUpdate['branding']['headerBgLight'] = '';
|
||||
$configUpdate['branding']['headerBgDark'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert configuration to JSON.
|
||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||
@@ -267,6 +323,18 @@ $public['onlyoffice'] = ['enabled' => $ooEnabled];
|
||||
return ["success" => "Configuration updated successfully."];
|
||||
}
|
||||
|
||||
private static function sanitizeColorHex($value): string
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') return '';
|
||||
|
||||
// allow #RGB or #RRGGBB
|
||||
if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $value)) {
|
||||
return strtoupper($value);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current configuration.
|
||||
*
|
||||
@@ -368,6 +436,25 @@ $public['onlyoffice'] = ['enabled' => $ooEnabled];
|
||||
$config['onlyoffice']['publicOrigin'] = self::sanitizeHttpUrl($config['onlyoffice']['publicOrigin'] ?? '');
|
||||
}
|
||||
|
||||
// Branding
|
||||
if (!isset($config['branding']) || !is_array($config['branding'])) {
|
||||
$config['branding'] = [
|
||||
'customLogoUrl' => '',
|
||||
'headerBgLight' => '',
|
||||
'headerBgDark' => '',
|
||||
];
|
||||
} else {
|
||||
$config['branding']['customLogoUrl'] = self::sanitizeLogoUrl(
|
||||
$config['branding']['customLogoUrl'] ?? ''
|
||||
);
|
||||
$config['branding']['headerBgLight'] = self::sanitizeColorHex(
|
||||
$config['branding']['headerBgLight'] ?? ''
|
||||
);
|
||||
$config['branding']['headerBgDark'] = self::sanitizeColorHex(
|
||||
$config['branding']['headerBgDark'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
@@ -393,6 +480,11 @@ $public['onlyoffice'] = ['enabled' => $ooEnabled];
|
||||
'docsOrigin' => '',
|
||||
'publicOrigin' => '',
|
||||
],
|
||||
'branding' => [
|
||||
'customLogoUrl' => '',
|
||||
'headerBgLight' => '',
|
||||
'headerBgDark' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user