+
+
`;
document.body.appendChild(overlay);
+ // theme the close “×” for visibility + hover rules that match your site:
+ const closeBtn = overlay.querySelector("#closeFileModal");
+ function paintCloseBase() {
+ closeBtn.style.backgroundColor = 'transparent';
+ closeBtn.style.color = '#e11d48'; // base red X
+ closeBtn.style.boxShadow = 'none';
+ }
+ function onCloseHoverEnter() {
+ const dark = document.documentElement.classList.contains('dark-mode');
+ closeBtn.style.backgroundColor = '#ef4444'; // red fill
+ closeBtn.style.color = dark ? '#000' : '#fff'; // X: black in dark / white in light
+ closeBtn.style.boxShadow = '0 0 6px rgba(239,68,68,.6)';
+ }
+ function onCloseHoverLeave() { paintCloseBase(); }
+ paintCloseBase();
+ closeBtn.addEventListener('mouseenter', onCloseHoverEnter);
+ closeBtn.addEventListener('mouseleave', onCloseHoverLeave);
+
function closeModal() {
try { overlay.querySelectorAll("video,audio").forEach(m => { try{m.pause()}catch(_){}}); } catch {}
if (overlay._onKey) window.removeEventListener('keydown', overlay._onKey);
overlay.remove();
}
- overlay.querySelector("#closeFileModal").addEventListener("click", closeModal);
+ closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); });
return overlay;
}
function setTitle(overlay, name) {
- const el = overlay.querySelector('.media-title-badge');
- if (el) el.textContent = name || '';
+ const textEl = overlay.querySelector('.title-text');
+ const iconEl = overlay.querySelector('.title-icon');
+ if (textEl) {
+ textEl.textContent = name || '';
+ textEl.setAttribute('title', name || '');
+ }
+ if (iconEl) {
+ iconEl.textContent = getIconForFile(name);
+ // keep the icon legible in both themes
+ const dark = document.documentElement.classList.contains('dark-mode');
+ iconEl.style.color = dark ? '#f5f5f5' : '#111111';
+ iconEl.style.opacity = dark ? '0.96' : '0.9';
+ }
}
-function makeMI(name, title) {
+// Topbar icon (theme-aware) used for image tools + video actions
+function makeTopIcon(name, title) {
const b = document.createElement('button');
- b.className = `material-icons ${name}`;
- b.textContent = name; // Material Icons font
+ b.className = 'material-icons';
+ b.textContent = name;
b.title = title;
+
+ const dark = document.documentElement.classList.contains('dark-mode');
+
Object.assign(b.style, {
- width: "32px",
- height: "32px",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- background: "rgba(0,0,0,.25)",
- border: "1px solid rgba(255,255,255,.25)",
- cursor: "pointer",
- userSelect: "none",
- fontSize: "20px",
- padding: "0",
- borderRadius: "8px",
- color: "#fff",
- lineHeight: "1"
+ width: '32px',
+ height: '32px',
+ borderRadius: '8px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ border: dark ? '1px solid rgba(255,255,255,.25)' : '1px solid rgba(0,0,0,.15)',
+ background: dark ? 'rgba(255,255,255,.14)' : 'rgba(0,0,0,.08)',
+ cursor: 'pointer',
+ fontSize: '20px',
+ lineHeight: '1',
+ color: dark ? '#f5f5f5' : '#111',
+ boxShadow: dark ? '0 1px 2px rgba(0,0,0,.6)' : '0 1px 1px rgba(0,0,0,.08)'
});
+
+ b.addEventListener('mouseenter', () => {
+ const darkNow = document.documentElement.classList.contains('dark-mode');
+ b.style.background = darkNow ? 'rgba(255,255,255,.22)' : 'rgba(0,0,0,.14)';
+ });
+ b.addEventListener('mouseleave', () => {
+ const darkNow = document.documentElement.classList.contains('dark-mode');
+ b.style.background = darkNow ? 'rgba(255,255,255,.14)' : 'rgba(0,0,0,.08)';
+ });
+
return b;
}
function setNavVisibility(overlay, showPrev, showNext) {
const prev = overlay.querySelector('.nav-left');
const next = overlay.querySelector('.nav-right');
- prev.style.display = showPrev ? 'inline-flex' : 'none';
- next.style.display = showNext ? 'inline-flex' : 'none';
+ prev.style.display = showPrev ? 'flex' : 'none';
+ next.style.display = showNext ? 'flex' : 'none';
}
function setRowWatchedBadge(name, watched) {
@@ -280,8 +352,8 @@ function setRowWatchedBadge(name, watched) {
export function previewFile(fileUrl, fileName) {
const overlay = ensureMediaModal();
const container = overlay.querySelector(".file-preview-container");
- const actionWrap = overlay.querySelector(".media-actions-bar .action-group");
- const statusChip = overlay.querySelector(".media-actions-bar .status-chip");
+ const actionWrap = overlay.querySelector(".media-right .action-group");
+ const statusChip = overlay.querySelector(".media-right .status-chip");
// replace nav buttons to clear old listeners
let prevBtn = overlay.querySelector('.nav-left');
@@ -320,10 +392,11 @@ export function previewFile(fileUrl, fileName) {
img.dataset.rotate = 0;
container.appendChild(img);
- const zoomInBtn = makeMI('zoom_in', t('zoom_in') || 'Zoom In');
- const zoomOutBtn = makeMI('zoom_out', t('zoom_out') || 'Zoom Out');
- const rotateLeft = makeMI('rotate_left', t('rotate_left') || 'Rotate Left');
- const rotateRight = makeMI('rotate_right', t('rotate_right') || 'Rotate Right');
+ // topbar-aligned, theme-aware icons
+ const zoomInBtn = makeTopIcon('zoom_in', t('zoom_in') || 'Zoom In');
+ const zoomOutBtn = makeTopIcon('zoom_out', t('zoom_out') || 'Zoom Out');
+ const rotateLeft = makeTopIcon('rotate_left', t('rotate_left') || 'Rotate Left');
+ const rotateRight = makeTopIcon('rotate_right', t('rotate_right') || 'Rotate Right');
actionWrap.appendChild(zoomInBtn);
actionWrap.appendChild(zoomOutBtn);
actionWrap.appendChild(rotateLeft);
@@ -405,14 +478,11 @@ export function previewFile(fileUrl, fileName) {
video.style.objectFit = "contain";
container.appendChild(video);
- const markBtn = document.createElement('button');
- const clearBtn = document.createElement('button');
- markBtn.className = 'btn btn-sm btn-success';
- clearBtn.className = 'btn btn-sm btn-secondary';
- markBtn.textContent = t("mark_as_viewed") || "Mark as viewed";
- clearBtn.textContent = t("clear_progress") || "Clear progress";
- actionWrap.appendChild(markBtn);
- actionWrap.appendChild(clearBtn);
+ // Top-right action icons (Material icons, theme-aware)
+ const markBtnIcon = makeTopIcon('check_circle', t("mark_as_viewed") || "Mark as viewed");
+ const clearBtnIcon = makeTopIcon('restart_alt', t("clear_progress") || "Clear progress");
+ actionWrap.appendChild(markBtnIcon);
+ actionWrap.appendChild(clearBtnIcon);
const videos = (Array.isArray(fileData) ? fileData : []).filter(f => VID_RE.test(f.name));
overlay.mediaType = 'video';
@@ -453,15 +523,14 @@ export function previewFile(fileUrl, fileName) {
if (!statusChip) return;
// Completed
if (state && state.completed) {
-
statusChip.textContent = (t('viewed') || 'Viewed') + ' ✓';
statusChip.style.display = 'inline-block';
statusChip.style.borderColor = 'rgba(34,197,94,.45)';
statusChip.style.background = 'rgba(34,197,94,.15)';
statusChip.style.color = '#22c55e';
- markBtn.style.display = 'none';
- clearBtn.style.display = '';
- clearBtn.textContent = t('reset_progress') || t('clear_progress') || 'Reset';
+ markBtnIcon.style.display = 'none';
+ clearBtnIcon.style.display = '';
+ clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
return;
}
// In progress
@@ -469,18 +538,20 @@ export function previewFile(fileUrl, fileName) {
const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100)));
statusChip.textContent = `${pct}%`;
statusChip.style.display = 'inline-block';
- statusChip.style.borderColor = 'rgba(250,204,21,.45)';
- statusChip.style.background = 'rgba(250,204,21,.15)';
- statusChip.style.color = '#facc15';
- markBtn.style.display = '';
- clearBtn.style.display = '';
- clearBtn.textContent = t('reset_progress') || t('clear_progress') || 'Reset';
+ const dark = document.documentElement.classList.contains('dark-mode');
+ const ORANGE_HEX = '#ea580c'; // darker orange (works in light/dark)
+ statusChip.style.color = ORANGE_HEX;
+ statusChip.style.borderColor = dark ? 'rgba(234,88,12,.55)' : 'rgba(234,88,12,.45)'; // #ea580c @ different alphas
+ statusChip.style.background = dark ? 'rgba(234,88,12,.18)' : 'rgba(234,88,12,.12)';
+ markBtnIcon.style.display = '';
+ clearBtnIcon.style.display = '';
+ clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
return;
}
// No progress
statusChip.style.display = 'none';
- markBtn.style.display = '';
- clearBtn.style.display = 'none';
+ markBtnIcon.style.display = '';
+ clearBtnIcon.style.display = 'none';
}
function bindVideoEvents(nm) {
@@ -494,8 +565,8 @@ export function previewFile(fileUrl, fileName) {
if (state && Number.isFinite(state.seconds) && state.seconds > 0 && state.seconds < (video.duration || Infinity)) {
video.currentTime = state.seconds;
const seconds = Math.floor(video.currentTime || 0);
-const duration = Math.floor(video.duration || 0);
-setFileProgressBadge(nm, seconds, duration);
+ const duration = Math.floor(video.duration || 0);
+ setFileProgressBadge(nm, seconds, duration);
showToast((t("resumed_from") || "Resumed from") + " " + Math.floor(state.seconds) + "s");
} else {
const ls = localStorage.getItem(lsKey(nm));
@@ -528,14 +599,14 @@ setFileProgressBadge(nm, seconds, duration);
renderStatus({ seconds: duration, duration, completed: true });
});
- markBtn.onclick = async () => {
+ markBtnIcon.onclick = async () => {
const duration = Math.floor(video.duration || 0);
await sendProgress({ nm, seconds: duration, duration, completed: true });
showToast(t("marked_viewed") || "Marked as viewed");
setFileWatchedBadge(nm, true);
renderStatus({ seconds: duration, duration, completed: true });
};
- clearBtn.onclick = async () => {
+ clearBtnIcon.onclick = async () => {
await sendProgress({ nm, seconds: 0, duration: null, completed: false, clear: true });
try { localStorage.removeItem(lsKey(nm)); } catch {}
showToast(t("progress_cleared") || "Progress cleared");
diff --git a/resources/dark-admin-panel.png b/resources/dark-admin-panel.png
index 602ce23..8b20caf 100644
Binary files a/resources/dark-admin-panel.png and b/resources/dark-admin-panel.png differ
diff --git a/resources/dark-gallery.png b/resources/dark-gallery.png
index 6d7010e..662526c 100644
Binary files a/resources/dark-gallery.png and b/resources/dark-gallery.png differ
diff --git a/resources/dark-header.png b/resources/dark-header.png
index 1cf2ee3..523d272 100644
Binary files a/resources/dark-header.png and b/resources/dark-header.png differ
diff --git a/resources/dark-preview.png b/resources/dark-preview.png
index c0cb54c..ff8ea40 100644
Binary files a/resources/dark-preview.png and b/resources/dark-preview.png differ
diff --git a/resources/dark-sidebar.png b/resources/dark-sidebar.png
index 886c3e6..968c340 100644
Binary files a/resources/dark-sidebar.png and b/resources/dark-sidebar.png differ
diff --git a/resources/filerise-v1.8.10-latest.gif b/resources/filerise-v1.8.10-latest.gif
new file mode 100644
index 0000000..9998131
Binary files /dev/null and b/resources/filerise-v1.8.10-latest.gif differ
diff --git a/resources/light-admin-panel.png b/resources/light-admin-panel.png
index 6c231bb..dd568e4 100644
Binary files a/resources/light-admin-panel.png and b/resources/light-admin-panel.png differ
diff --git a/resources/light-drag-file.png b/resources/light-drag-file.png
index 1ce2570..74d2a62 100644
Binary files a/resources/light-drag-file.png and b/resources/light-drag-file.png differ
diff --git a/resources/light-preview.png b/resources/light-preview.png
index df8ecde..ea0362a 100644
Binary files a/resources/light-preview.png and b/resources/light-preview.png differ
diff --git a/resources/light-share.png b/resources/light-share.png
index 3394cfe..c3358f7 100644
Binary files a/resources/light-share.png and b/resources/light-share.png differ
diff --git a/resources/light-topbar.png b/resources/light-topbar.png
index ffeb718..42b5771 100644
Binary files a/resources/light-topbar.png and b/resources/light-topbar.png differ
diff --git a/resources/light-trash.png b/resources/light-trash.png
index ae5c08d..cdbeb53 100644
Binary files a/resources/light-trash.png and b/resources/light-trash.png differ
diff --git a/resources/light-user-panel.png b/resources/light-user-panel.png
index e268247..ce5e1c1 100644
Binary files a/resources/light-user-panel.png and b/resources/light-user-panel.png differ
diff --git a/src/controllers/OnlyOfficeController.php b/src/controllers/OnlyOfficeController.php
index 1c4d6cc..6b25130 100644
--- a/src/controllers/OnlyOfficeController.php
+++ b/src/controllers/OnlyOfficeController.php
@@ -16,6 +16,23 @@ private const OO_SUPPORTED_EXTS = [
'ppt','pptx','odp',
'pdf'
];
+
+/** Origin that the Document Server should use to reach FileRise fast (internal URL) */
+private function effectiveFileOriginForDocs(): string
+{
+ $cfg = AdminModel::getConfig();
+ $oo = is_array($cfg['onlyoffice'] ?? null) ? $cfg['onlyoffice'] : [];
+
+ // 1) explicit constant
+ if (defined('ONLYOFFICE_FILE_ORIGIN_FOR_DOCS') && ONLYOFFICE_FILE_ORIGIN_FOR_DOCS !== '') {
+ return (string)ONLYOFFICE_FILE_ORIGIN_FOR_DOCS;
+ }
+ // 2) admin.json setting
+ if (!empty($oo['fileOriginForDocs'])) return (string)$oo['fileOriginForDocs'];
+
+ // 3) fallback: whatever the public sees (may hairpin, but still works)
+ return $this->effectivePublicOrigin();
+}
// Never editable via OO (we’ll always set edit=false for these)
private const OO_NEVER_EDIT = ['pdf'];
@@ -127,117 +144,119 @@ private function ooLog(string $level, string $msg): void
/** GET /api/onlyoffice/status.php */
public function status(): void
- {
- header('Content-Type: application/json; charset=utf-8');
- header('Cache-Control: no-store');
+{
+ header('Content-Type: application/json; charset=utf-8');
+ header('Cache-Control: no-store');
- $enabled = $this->effectiveEnabled();
- $docsOrig = $this->effectiveDocsOrigin();
- $secret = $this->effectiveSecret();
+ $enabled = $this->effectiveEnabled();
+ $docsOrig = $this->effectiveDocsOrigin();
+ $secret = $this->effectiveSecret();
- // Must have docs origin and secret to actually function
- $enabled = $enabled && ($docsOrig !== '') && ($secret !== '');
+ // Must have docs origin and secret to actually function
+ $enabled = $enabled && ($docsOrig !== '') && ($secret !== '');
- $exts = self::OO_SUPPORTED_EXTS;
- // If you want the extras:
- $exts = array_values(array_unique(array_merge($exts, self::OO_VIEW_ONLY_EXTRAS)));
-
- echo json_encode(['enabled' => (bool)$enabled, 'exts' => $exts], JSON_UNESCAPED_SLASHES);
- }
+ $exts = self::OO_SUPPORTED_EXTS;
+ $exts = array_values(array_unique(array_merge($exts, self::OO_VIEW_ONLY_EXTRAS)));
+
+ echo json_encode([
+ 'enabled' => (bool)$enabled,
+ 'exts' => $exts,
+ 'docsOrigin' => $docsOrig, // <-- for preconnect/api.js
+ 'publicOrigin' => $this->effectivePublicOrigin() // <-- informational
+ ], JSON_UNESCAPED_SLASHES);
+}
/** GET /api/onlyoffice/config.php?folder=...&file=... */
- public function config(): void
- {
- header('Content-Type: application/json; charset=utf-8');
- header('Cache-Control: no-store');
+ // --- config(): use the DocServer-facing origin for fileUrl & callbackUrl ---
+public function config(): void
+{
+ header('Content-Type: application/json; charset=utf-8');
+ header('Cache-Control: no-store');
- @session_start();
- $user = $_SESSION['username'] ?? 'anonymous';
- $perms = [];
- $isAdmin = \ACL::isAdmin($perms);
+ @session_start();
+ $user = $_SESSION['username'] ?? 'anonymous';
+ $perms = [];
+ $isAdmin = \ACL::isAdmin($perms);
- // Effective toggles
- $enabled = $this->effectiveEnabled();
- $docsOrigin = rtrim($this->effectiveDocsOrigin(), '/');
- $secret = $this->effectiveSecret();
- if (!$enabled) { http_response_code(404); echo '{"error":"ONLYOFFICE disabled"}'; return; }
- if ($secret === '') { http_response_code(500); echo '{"error":"ONLYOFFICE_JWT_SECRET not configured"}'; return; }
- if ($docsOrigin === '') { http_response_code(500); echo '{"error":"ONLYOFFICE_DOCS_ORIGIN not configured"}'; return; }
- if (!defined('UPLOAD_DIR')) { http_response_code(500); echo '{"error":"UPLOAD_DIR not defined"}'; return; }
+ $enabled = $this->effectiveEnabled();
+ $docsOrigin = rtrim($this->effectiveDocsOrigin(), '/');
+ $secret = $this->effectiveSecret();
- // Inputs
- $folder = \ACL::normalizeFolder((string)($_GET['folder'] ?? 'root'));
- $file = basename((string)($_GET['file'] ?? ''));
- if ($file === '') { http_response_code(400); echo '{"error":"Bad request"}'; return; }
+ if (!$enabled) { http_response_code(404); echo '{"error":"ONLYOFFICE disabled"}'; return; }
+ if ($secret === '') { http_response_code(500); echo '{"error":"ONLYOFFICE_JWT_SECRET not configured"}'; return; }
+ if ($docsOrigin === '') { http_response_code(500); echo '{"error":"ONLYOFFICE_DOCS_ORIGIN not configured"}'; return; }
+ if (!defined('UPLOAD_DIR')) { http_response_code(500); echo '{"error":"UPLOAD_DIR not defined"}'; return; }
- // ACL
- if (!\ACL::canRead($user, $perms, $folder)) { http_response_code(403); echo '{"error":"Forbidden"}'; return; }
- $canEdit = \ACL::canEdit($user, $perms, $folder);
+ $folder = \ACL::normalizeFolder((string)($_GET['folder'] ?? 'root'));
+ $file = basename((string)($_GET['file'] ?? ''));
+ if ($file === '') { http_response_code(400); echo '{"error":"Bad request"}'; return; }
- // Path
- $base = rtrim(UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR;
- $rel = ($folder === 'root') ? '' : ($folder . '/');
- $abs = realpath($base . $rel . $file);
- if (!$abs || !is_file($abs)) { http_response_code(404); echo '{"error":"Not found"}'; return; }
- if (strpos($abs, realpath($base)) !== 0) { http_response_code(400); echo '{"error":"Invalid path"}'; return; }
+ if (!\ACL::canRead($user, $perms, $folder)) { http_response_code(403); echo '{"error":"Forbidden"}'; return; }
+ $canEdit = \ACL::canEdit($user, $perms, $folder);
- // Public origin
- $publicOrigin = $this->effectivePublicOrigin();
+ $base = rtrim(UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR;
+ $rel = ($folder === 'root') ? '' : ($folder . '/');
+ $abs = realpath($base . $rel . $file);
+ if (!$abs || !is_file($abs)) { http_response_code(404); echo '{"error":"Not found"}'; return; }
+ if (strpos($abs, realpath($base)) !== 0) { http_response_code(400); echo '{"error":"Invalid path"}'; return; }
- // Signed download
- $exp = time() + 10*60;
- $data = json_encode(['f'=>$folder,'n'=>$file,'u'=>$user,'adm'=>$isAdmin,'exp'=>$exp], JSON_UNESCAPED_SLASHES);
- $sig = hash_hmac('sha256', $data, $secret, true);
- $tok = $this->b64uEnc($data) . '.' . $this->b64uEnc($sig);
- $fileUrl = $publicOrigin . '/api/onlyoffice/signed-download.php?tok=' . rawurlencode($tok);
+ // IMPORTANT: use the internal/fast origin for DocServer fetch + callback
+ $fileOriginForDocs = rtrim($this->effectiveFileOriginForDocs(), '/');
- // Callback
- $cbExp = time() + 10*60;
- $cbSig = hash_hmac('sha256', $folder.'|'.$file.'|'.$cbExp, $secret);
- $callbackUrl = $publicOrigin . '/api/onlyoffice/callback.php'
- . '?folder=' . rawurlencode($folder)
- . '&file=' . rawurlencode($file)
- . '&exp=' . $cbExp
- . '&sig=' . $cbSig;
+ $exp = time() + 10*60;
+ $data = json_encode(['f'=>$folder,'n'=>$file,'u'=>$user,'adm'=>$isAdmin,'exp'=>$exp], JSON_UNESCAPED_SLASHES);
+ $sig = hash_hmac('sha256', $data, $secret, true);
+ $tok = $this->b64uEnc($data) . '.' . $this->b64uEnc($sig);
+ $fileUrl = $fileOriginForDocs . '/api/onlyoffice/signed-download.php?tok=' . rawurlencode($tok);
- // Doc type & key
- $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION) ?: 'docx');
- $docType = in_array($ext, ['xls','xlsx','ods','csv'], true) ? 'cell'
- : (in_array($ext, ['ppt','pptx','odp'], true) ? 'slide' : 'word');
- $key = substr(sha1($abs . '|' . (string)filemtime($abs)), 0, 20);
+ $cbExp = time() + 10*60;
+ $cbSig = hash_hmac('sha256', $folder.'|'.$file.'|'.$cbExp, $secret);
+ $callbackUrl = $fileOriginForDocs . '/api/onlyoffice/callback.php'
+ . '?folder=' . rawurlencode($folder)
+ . '&file=' . rawurlencode($file)
+ . '&exp=' . $cbExp
+ . '&sig=' . $cbSig;
- $docsApiJs = $docsOrigin . '/web-apps/apps/api/documents/api.js';
+ $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION) ?: 'docx');
+ $docType = in_array($ext, ['xls','xlsx','ods','csv'], true) ? 'cell'
+ : (in_array($ext, ['ppt','pptx','odp'], true) ? 'slide' : 'word');
+ $key = substr(sha1($abs . '|' . (string)filemtime($abs)), 0, 20);
- $cfgOut = [
- 'document' => [
- 'fileType' => $ext,
- 'key' => $key,
- 'title' => $file,
- 'url' => $fileUrl,
- 'permissions' => [
- 'download' => true,
- 'print' => true,
- 'edit' => $canEdit && !in_array($ext, self::OO_NEVER_EDIT, true),
- ],
- ],
- 'documentType' => $docType,
- 'editorConfig' => [
- 'callbackUrl' => $callbackUrl,
- 'user' => ['id'=>$user, 'name'=>$user],
- 'lang' => 'en',
- ],
- 'type' => 'desktop',
- ];
+ $docsApiJs = $docsOrigin . '/web-apps/apps/api/documents/api.js';
- // JWT sign cfg
- $h = $this->b64uEnc(json_encode(['alg'=>'HS256','typ'=>'JWT']));
- $p = $this->b64uEnc(json_encode($cfgOut, JSON_UNESCAPED_SLASHES));
- $s = $this->b64uEnc(hash_hmac('sha256', "$h.$p", $secret, true));
- $cfgOut['token'] = "$h.$p.$s";
- $cfgOut['docs_api_js'] = $docsApiJs;
+ $cfgOut = [
+ 'document' => [
+ 'fileType' => $ext,
+ 'key' => $key,
+ 'title' => $file,
+ 'url' => $fileUrl,
+ 'permissions' => [
+ 'download' => true,
+ 'print' => true,
+ 'edit' => $canEdit && !in_array($ext, self::OO_NEVER_EDIT, true),
+ ],
+ ],
+ 'documentType' => $docType,
+ 'editorConfig' => [
+ 'callbackUrl' => $callbackUrl,
+ 'user' => ['id'=>$user, 'name'=>$user],
+ 'lang' => 'en',
+ ],
+ 'type' => 'desktop',
+ ];
- echo json_encode($cfgOut, JSON_UNESCAPED_SLASHES);
- }
+ // JWT sign cfg
+ $h = $this->b64uEnc(json_encode(['alg'=>'HS256','typ'=>'JWT']));
+ $p = $this->b64uEnc(json_encode($cfgOut, JSON_UNESCAPED_SLASHES));
+ $s = $this->b64uEnc(hash_hmac('sha256', "$h.$p", $secret, true));
+ $cfgOut['token'] = "$h.$p.$s";
+
+ // expose to client for preconnect/script load
+ $cfgOut['docs_api_js'] = $docsApiJs;
+ $cfgOut['documentServerOrigin'] = $docsOrigin;
+
+ echo json_encode($cfgOut, JSON_UNESCAPED_SLASHES);
+}
/** POST /api/onlyoffice/callback.php?folder=...&file=...&exp=...&sig=... */
public function callback(): void
@@ -343,41 +362,52 @@ private function ooLog(string $level, string $msg): void
/** GET /api/onlyoffice/signed-download.php?tok=... */
public function signedDownload(): void
- {
- header('X-Content-Type-Options: nosniff');
- header('Cache-Control: no-store');
+{
+ header('X-Content-Type-Options: nosniff');
+ header('Cache-Control: no-store');
- $secret = $this->effectiveSecret();
- if ($secret === '') { http_response_code(403); return; }
+ $secret = $this->effectiveSecret();
+ if ($secret === '') { http_response_code(403); return; }
- $tok = $_GET['tok'] ?? '';
- if (!$tok || strpos($tok, '.') === false) { http_response_code(400); return; }
- [$b64data, $b64sig] = explode('.', $tok, 2);
- $data = $this->b64uDec($b64data);
- $sig = $this->b64uDec($b64sig);
- if ($data === false || $sig === false) { http_response_code(400); return; }
+ $tok = $_GET['tok'] ?? '';
+ if (!$tok || strpos($tok, '.') === false) { http_response_code(400); return; }
+ [$b64data, $b64sig] = explode('.', $tok, 2);
+ $data = $this->b64uDec($b64data);
+ $sig = $this->b64uDec($b64sig);
+ if ($data === false || $sig === false) { http_response_code(400); return; }
- $calc = hash_hmac('sha256', $data, $secret, true);
- if (!hash_equals($calc, $sig)) { http_response_code(403); return; }
+ $calc = hash_hmac('sha256', $data, $secret, true);
+ if (!hash_equals($calc, $sig)) { http_response_code(403); return; }
- $payload = json_decode($data, true);
- if (!$payload || !isset($payload['f'],$payload['n'],$payload['exp'])) { http_response_code(400); return; }
- if (time() > (int)$payload['exp']) { http_response_code(403); return; }
+ $payload = json_decode($data, true);
+ if (!$payload || !isset($payload['f'],$payload['n'],$payload['exp'])) { http_response_code(400); return; }
+ if (time() > (int)$payload['exp']) { http_response_code(403); return; }
- $folder = trim(str_replace('\\','/',$payload['f']),"/ \t\r\n");
- if ($folder === '' || $folder === 'root') $folder = 'root';
- $file = basename((string)$payload['n']);
+ $folder = trim(str_replace('\\','/',$payload['f']),"/ \t\r\n");
+ if ($folder === '' || $folder === 'root') $folder = 'root';
+ $file = basename((string)$payload['n']);
- $base = rtrim(UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR;
- $rel = ($folder === 'root') ? '' : ($folder . '/');
- $abs = realpath($base . $rel . $file);
- if (!$abs || !is_file($abs)) { http_response_code(404); return; }
- if (strpos($abs, realpath($base)) !== 0) { http_response_code(400); return; }
+ $base = rtrim(UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR;
+ $rel = ($folder === 'root') ? '' : ($folder . '/');
+ $abs = realpath($base . $rel . $file);
+ if (!$abs || !is_file($abs)) { http_response_code(404); return; }
+ if (strpos($abs, realpath($base)) !== 0) { http_response_code(400); return; }
- $mime = mime_content_type($abs) ?: 'application/octet-stream';
- header('Content-Type: '.$mime);
- header('Content-Length: '.filesize($abs));
- header('Content-Disposition: inline; filename="' . rawurlencode($file) . '"');
- readfile($abs);
+ // Common headers
+ $mime = mime_content_type($abs) ?: 'application/octet-stream';
+ $len = filesize($abs);
+ header('Content-Type: '.$mime);
+ header('Content-Length: '.$len);
+ header('Content-Disposition: inline; filename="' . rawurlencode($file) . '"');
+ header('Accept-Ranges: none'); // OO doesn’t require ranges; avoids partial edge-cases
+
+ // ---- Key change: for HEAD, do NOT read the file ----
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD') {
+ // send headers only; no body
+ return;
}
+
+ // GET → stream the file
+ readfile($abs);
+}
}
\ No newline at end of file