Skip to content

Feat/overview and wopi#7

Draft
moodyjmz wants to merge 56 commits into
mainfrom
feat/overview-and-wopi
Draft

Feat/overview and wopi#7
moodyjmz wants to merge 56 commits into
mainfrom
feat/overview-and-wopi

Conversation

@moodyjmz
Copy link
Copy Markdown

Combined overview and WOPI for testing only - better to work on separate branches

moodyjmz added 30 commits May 22, 2026 19:01
Add @mdi/js, @nextcloud/{auth,files,l10n,router}, and axios needed for the
overview feature. Bump nextcloud min-version to 33 / max-version to 35 to
match the APIs the overview relies on.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Port the three services from richdocuments to TypeScript with full type
definitions. officeFiles.ts uses DAV SEARCH (depth:infinity) with a
MAX_DISPLAY_FILES render guard; config.ts persists grid-view preference to
localStorage; templates.ts wraps the NC core templates OCS API.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Port FileCard (slot-based card with preview/icon/name/subname) and
TemplateSection (scrollable template row with ResizeObserver arrows and
per-type gradient backgrounds) from richdocuments to Vue 3 script setup
with TypeScript. Replaces vue-material-design-icons chevrons with @mdi/js
paths in TemplateSection.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Port OfficeOverview from richdocuments Options API to Vue 3 script setup
with TypeScript. All vue-material-design-icons icons replaced with @mdi/js
paths via NcIconSvgWrapper. App-id references switched from richdocuments
to office. previewEnabled state dropped; per-card @error fallback handles
missing previews. Grid-view mode read from localStorage via config.ts.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- Override vue-eslint-parser to use @typescript-eslint/parser as inner
  parser so TypeScript generics in <script setup lang="ts"> are accepted
- Remove direct `webdav` import from officeFiles.ts; use inline structural
  type instead — `webdav` is a transitive dep and the import was type-only
- Add .stylelintrc.cjs extending @nextcloud/stylelint-config (was absent)
- Update plan doc: axios → @nextcloud/axios correction

All three checks now pass clean: eslint, stylelint, tsc --noEmit.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- Add src/global.d.ts to type window.OCA.Viewer
- Replace regex mime→theme detection in TemplateSection with an explicit MIME_THEME map
- Extract OcsErrorResponse type to templates.ts and use it in the catch block
- Add activeCategoryName computed, guard files section with v-else-if="activeCreator"
  to eliminate the three activeCreator! non-null assertions
- Add activeFilter intentionally-not-reset comment in watch(activeCreator)
- Prefer component.focus() over $el.querySelector in the dialog nextTick
- Add validateFilename() to block / \\ and NUL chars before the API call
- Document the flat cache design constraint in officeFiles.ts
- Remove dead restoreCreator parameter from fetchAll
- Add TTI comment on the module-level fetchAll() call

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…tion

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…n validation

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
After stripping WOPI template placeholders the URL may have no '?'
yet, so appending with '&' produced a malformed URL like
`https://editor/path&wopisrc=...`. Check for an existing '?' and
use '?' as the separator when absent.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
File extensions are user-controlled (derived from the filename) and
were interpolated directly into XPath predicates. Validate the extension
against a strict [a-zA-Z0-9]{1,20} allowlist before use.

Also pass LIBXML_NONET | LIBXML_NOCDATA to SimpleXMLElement to block
network requests during XML parsing of the discovery response.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
An unconstrained wopi_url allowed any scheme (file://, gopher://)
and any host including cloud metadata endpoints. Reject non-http/https
schemes and reject URLs that embed credentials.

allow_local_address remains enabled intentionally: self-hosted deployments
often run the editor and Nextcloud on the same host/network.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
moodyjmz added 26 commits May 22, 2026 23:58
Range requests with start >= fileSize now return 416 (Range Not
Satisfiable) per RFC 7233 §4.4 instead of silently streaming wrong
data. Added try/finally around both range-stream handles so they
are closed on exception.

Also wrapped php://input in try/finally in putFile so the handle
is always released even when early-return or an exception fires.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Without a cleanup job the office_wopi table grows without bound —
one row per file open per 10-hour TTL window. Add a TimedJob that
runs hourly and batch-deletes expired rows (500 at a time) via a new
WopiMapper::deleteByIds() helper.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
WOPI locks are file-level (one opaque lock_id per file, 30-minute TTL)
and distinct from the per-token session data in office_wopi. Separate
table keeps the semantics clean.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
WopiLock stores a single opaque lock_id per file with a 30-minute TTL
(WOPI spec). WopiLockMapper provides findByFileId, upsertLock (create
or refresh), getExpiredLockIds, and deleteByIds for the cleanup job.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
POST wopi/files/{fileId} dispatches on X-WOPI-Override header:
- LOCK: acquire lock; idempotent for same lock_id; 409 on conflict
- UNLOCK: verify lock_id then release; 409 on mismatch
- REFRESH_LOCK: extend TTL; 409 on mismatch
- GET_LOCK: return current lock_id (empty string if unlocked)
- LOCK + X-WOPI-OldLock: UnlockAndRelock (atomic swap)

All conflict responses carry X-WOPI-Lock and X-WOPI-LockFailureReason
headers per WOPI spec so the editor can surface a meaningful error.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Without these guards PutFile accepted any write regardless of whether
another client held the file lock, silently discarding concurrent edits.

- X-WOPI-Lock must match the current stored lock when one exists;
  returns 409 with X-WOPI-Lock + X-WOPI-LockFailureReason on mismatch.
- X-WOPI-ItemVersion (when provided) must match the file mtime; returns
  409 with current X-WOPI-ItemVersion when file changed out-of-band.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
The same hourly job now clears both office_wopi (expired tokens)
and office_wopi_locks (expired file locks) so neither table grows
without bound.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
NC35 removed registerJob() and registerSettings() from
IRegistrationContext. Declare the CleanupJob background job and the
Admin settings class in appinfo/info.xml instead, which is the
supported mechanism from NC35 onwards.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
NC's putContent() closes the stream handle internally. The finally
block called fclose() on an already-invalid resource, producing a
PHP warning and a 500 response. Guard with is_resource() before
closing.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…ponses

StreamResponse defaults to text/html; override on all GetFile paths
(full, range, zero-byte) for WOPI spec compliance.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
executeOperation dispatched LOCK/UNLOCK/REFRESH_LOCK without checking
canwrite, allowing read-only tokens (including read-only guest shares)
to acquire and hold locks.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Migration 1002 adds hide_download BOOLEAN (default false) to
oc_office_wopi. WopiMapper::generateGuestToken and TokenManager::
generateGuestToken accept the flag; CheckFileInfo derives
HideExportOption, DisablePrint, DisableExport and UserCanNotWriteRelative
from it at response time.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
GET /apps/office/open/share/{shareToken} opens a file in the WOPI editor
via a Nextcloud public share link. Handles:
- File shares (share node is a File)
- Folder shares (resolve target by fileId or path parameter)
- Password-protected shares (checks public_link_authenticated session
  key in both legacy string and current array-of-IDs formats)

Generates a guest WOPI token stamped with the share's canWrite and
hideDownload flags. Returns the same editor TemplateResponse shape
as EditorController::open.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
HideExportOption, DisablePrint and DisableExport are now derived from
the token's hide_download field. UserCanNotWriteRelative is true for
both guests and hideDownload tokens, preventing Save As from writing
back to the owner's storage.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- Strip control chars and cap guestName at 64 bytes in ShareController
  to prevent log injection and oversized display names
- Add boundary comment on getFirstNodeById in resolveFile
- Document three known gaps in PHASE3_DECISIONS.md (password UI,
  authenticated-user-through-share, federated shares)

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Parses app/@name attributes from the cached discovery XML to return
all MIME types the editor advertises. Returns [] on cache miss or
parse failure so callers degrade gracefully.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
KG2: authenticated NC users visiting a share link now get a full user
token via TokenManager::generateToken() rather than a guest token.
hideDownload is not applied on the user path — download restrictions
target unauthenticated third parties, not collaborators with direct access.

KG1: password-protected share without a session now redirects to
/s/{token} for the NC password challenge instead of returning 401 JSON.
Authenticated users bypass the password check entirely.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Register LoadAdditionalScriptsListener to load the office-file-actions
script on every Files app page. Supported MIME types from DiscoveryService
are injected as initial state so the file action can filter correctly
without a per-file HTTP round-trip.

Falls back to an empty MIME list if discovery is unavailable, preventing
the listener from blocking page render.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Adds a file-actions.ts entry that registers a DEFAULT file action for
all MIME types advertised by the editor's discovery XML. MIME list is
injected as initial state by LoadAdditionalScriptsListener.

- Authenticated file access routes to EditorController::open
- Public share context (isPublicShare()) routes to ShareController
  using getSharingToken() from @nextcloud/sharing/public

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Combines feat/euro-office-overview (file/template overview UI) with
feat/wopi-phase-4 (WOPI connector backend). Resolved conflicts in
info.xml (min-version 33) and package.json (union of dependencies).

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Avoids routing through the Files shortlink (/f/{fileid}) which loses
the overview as the referrer and lands the user in the Files app URL.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
window.close() only works for popup windows. For full-page navigation,
fall back to the office overview if there is no history to go back to.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
The initial skeleton used a cloud/weather icon. Replace with the
standard NC document (folded-corner page) icon to match the app's purpose.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Read the editor URL from NC initial state (injected by PageController)
instead of hardcoding /apps/office/open. When no editor URL is provided
(overview-only deployment), fall back to NC's /f/{fileid} file shortlink
which routes through the Files app and triggers whatever default file
action is registered (e.g. eurooffice).

This removes the cross-branch dependency introduced in the previous
navigation fix: the overview branch no longer assumes the WOPI editor
route exists. The combined/WOPI branch will provide the concrete URL
via PageController::index().

Behaviour summary:
- With WOPI backend: direct navigation to editor, history.back() returns
  to overview
- Without WOPI (e.g. eurooffice): routes via /f/{fileid} → Files app →
  registered default action; return-to-overview depends on that editor

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
The overview Vue component reads 'editor-url' from NC initial state and
falls back to /f/{fileid} when null. On this combined branch the WOPI
EditorController exists, so inject the concrete route so the overview
navigates directly to the editor with clean history.back() behaviour.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Documents the complete user flow, WOPI protocol sequence, key classes,
local dev setup, and Phase 3 share support with known gaps.

Signed-off-by: James Manuel <james.manuel@nextcloud.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz self-assigned this May 24, 2026
@moodyjmz moodyjmz marked this pull request as draft May 24, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant