feat(wopi): add WOPI protocol support for collaborative editing

Implement the Web Application Open Platform Interface (WOPI) protocol
to enable collaborative document editing with Collabora Online and
OnlyOffice through OxiCloud.

Backend:
- WOPI token service with HMAC-SHA256 signed access tokens
- WOPI lock service with in-memory lock management and expiry
- WOPI discovery service for auto-detecting editor capabilities
- WOPI HTTP handler: CheckFileInfo, GetFile, PutFile, Lock/Unlock
- File entity extended with owner_id for WOPI file-info responses
- Configuration via WOPI_* environment variables
- Services wired through DI in AppState

Frontend:
- WOPI editor component with modal and new-tab viewing modes
- Context menu integration for opening files in online editors
- Inline viewer integration for document preview

Infrastructure:
- Docker Compose file for local Collabora/OnlyOffice dev setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jan Wiebe
2026-02-21 13:39:27 +01:00
parent 807370e194
commit 4e2c9d2592
20 changed files with 1877 additions and 14 deletions
+34 -1
View File
@@ -15,6 +15,23 @@ const contextMenus = {
: (isFavorite ? 'Remove from favorites' : 'Add to favorites');
},
/**
* Show or hide WOPI editor options based on current target file
*/
async syncWopiOptionVisibility() {
const wopiEdit = document.getElementById('wopi-edit-file-option');
const wopiEditTab = document.getElementById('wopi-edit-file-tab-option');
if (!wopiEdit || !wopiEditTab) return;
const targetFile = window.app && window.app.contextMenuTargetFile;
const show = targetFile &&
window.wopiEditor &&
await window.wopiEditor.canEdit(targetFile.name);
wopiEdit.style.display = show ? '' : 'none';
wopiEditTab.style.display = show ? '' : 'none';
},
syncFavoriteOptionLabels() {
if (!window.favorites) return;
@@ -137,7 +154,23 @@ const contextMenus = {
}
window.ui.closeFileContextMenu();
});
document.getElementById('wopi-edit-file-option').addEventListener('click', () => {
if (window.app.contextMenuTargetFile) {
const file = window.app.contextMenuTargetFile;
window.wopiEditor.openInModal(file.id, file.name, 'edit');
}
window.ui.closeFileContextMenu();
});
document.getElementById('wopi-edit-file-tab-option').addEventListener('click', () => {
if (window.app.contextMenuTargetFile) {
const file = window.app.contextMenuTargetFile;
window.wopiEditor.openInTab(file.id, file.name, 'edit');
}
window.ui.closeFileContextMenu();
});
document.getElementById('download-file-option').addEventListener('click', () => {
if (window.app.contextMenuTargetFile) {
window.fileOps.downloadFile(