Repository navigation
Conversation
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>
…urface 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>
… putFile 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>
…lags
- Add RENAME_FILE handler in executeOperation: decodes UTF-7 header,
checks lock, rejects existing-name conflicts, moves file in-scope of
ILockManager, returns {"Name": "<name.ext>"}
- Fix CheckFileInfo: add SupportsUpdate, SupportsGetLock,
SupportsExtendedLockLength (lock_id VARCHAR(1024)), IsAnonymousUser
for guest sessions
- Set UserCanNotWriteRelative unconditionally to defer PutRelativeFile
(Save As) to Phase 6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- Replace hardcoded container name with `docker compose exec` pattern - Rename "eurooffice" to "connector app" in setup instructions - Remove private internal file references - Remove branch-specific cross-references - Add WOPI spec compliance section: operations table and CheckFileInfo capability flags table (reflects Phase 5 state) - Update features list and known gaps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Application::APP_ID was referenced in addInitScript() without an import, which would cause a fatal at runtime. Also trim one stale baseline entry revealed by the fix and add the missing createTable baseline entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…TOCTOU - RenameFile: validate decoded UTF-7 name with basename() — a name containing '/' or '\0' was silently used to construct an arbitrary target path, allowing files to escape their parent directory (C-1) - PutFile: require an active WOPI lock when saving a non-empty file, per MS-WOPI spec §3.3.5.3; returns 409 + X-WOPI-Lock: '' otherwise (H-3) - WopiLockMapper::upsertLock: catch REASON_UNIQUE_CONSTRAINT_VIOLATION on INSERT and retry as UPDATE to handle concurrent lock acquisition (H-2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…kenManager TokenManager::__construct is now correctly detected as used via constructor injection type hints in the controllers, so the suppression is no longer needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
ApiRoute registers under /ocs/v2.php/, but the admin UI fetches /index.php/apps/office/settings/admin - the endpoints were never reachable from the settings page. Also reset the discovery cache on settings save: the cached discovery XML depends on wopi_url and was served stale for up to an hour after changing the editor server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…ments Docker/proxy setups expose three distinct origins: NC->editor (wopi_url), browser->editor (public_wopi_url, rewrites the discovery urlsrc origin), and editor->NC (callback_url, rewrites the WOPISrc origin). Mirrors the richdocuments URL split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…on tests tests/bootstrap.php required the Nextcloud server tree and called OC_App::loadApp, which only resolves inside a server checkout. Add tests/bootstrap-unit.php (plain composer autoload, no server dependency) for tests/phpunit.xml, and rename the old bootstrap to bootstrap-integration.php for the new tests/phpunit.integration.xml, which runs tests/integration/ inside a server tree. Standalone unit tests also need OCP\* classes (IRequest, IAppConfig, etc.) for mocking, which vendor/nextcloud/ocp does not autoload on its own - add an autoload-dev psr-4 mapping for OCP\ and OCA\Office\Testsand regenerate the autoloader. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Cover get()/fetch()/resetCache() cache behavior and fetch-failure handling, getUrlSrc() (including the XPath-injection extension guard and the OnlyOffice-vs-Collabora getSupportedMimeTypes() MIME-detection gap, documented not fixed), and every buildEditorUrl() branch (public_wopi_url swap, callback_url swap, placeholder stripping, separator selection). Fixtures model OnlyOffice-style discovery (apps named by product, e.g. "Word") and Collabora-style discovery (apps named by MIME type). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
74 rows covering the implemented WOPI surface (CheckFileInfo, GetFile including range handling, PutFile lock/version conflicts, the lock protocol, RenameFile, tokens, routing) plus explicit not-implemented rows for spec operations the app does not support yet (PutRelativeFile, DeleteFile, PutUserInfo, proof-key validation, ItemVersion completeness, access_token form-POST delivery), so gaps stay visible rather than implicit. tests/spec-coverage.php prints per-operation and total counts with percentages and exits non-zero if any row claims tested without a named test. Rows flip to tested in the same commit as the test that covers them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
30 test methods driving executeOperation() end to end (mocked WopiMapper/WopiLockMapper/ILockManager/IRootFolder, no server tree needed): the read-only-token 403 guard across all five overrides, the 501 unknown-override path, LOCK/UNLOCK/REFRESH_LOCK/GET_LOCK including UnlockAndRelock and expired-lock-treated-as-absent semantics, and a full RenameFile suite - guest 403, empty/invalid name 400, name-collision 400, lock-mismatch 409, file-not-found 404, success 200. Also documents (without fixing) that the RenameFile path-traversal guard's basename() check only rejects '/' and null bytes, not '\' - basename() treats '\' as a literal character on non-Windows PHP, so a backslash-based name currently passes the guard unmodified. Response::getHeaders() pulls in framework defaults via \OCP\Server::get(), which needs a running server and isn't available standalone; read the private $headers property via reflection instead to check only the WOPI headers the controller itself set. Flips 30 wopi-spec-matrix.php rows from implemented-untested to tested, pointing at the covering test method. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
15 test methods for generateToken() and generateGuestToken(): serverHost capture from IURLGenerator, fileId/ownerUid/editorUid/ version/canWrite passed through to the mocked WopiMapper, owner-UID fallback to the current user when the file has no owner, guest-token propagation of guestName/canWrite/hideDownload and folder resolution via the file owner rather than the current session user, BeforeNodeReadEvent dispatch, and NotPermittedException on an unreadable or missing file. Does not test token randomness/length or expiry value sanity - both live in WopiMapper::generateFileToken()/generateGuestToken() (ISecureRandom and TOKEN_TTL), not TokenManager. With WopiMapper mocked there is nothing of theirs left to exercise through this class; see the class docblock for detail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Two bugs in the integration bootstrap: 1. __DIR__-relative paths break when the app directory is bind-mounted separately from the server tree: realpath() reports the bind-mount source, so relative traversal to the server root is unreliable. Use /var/www/html directly, overridable via NEXTCLOUD_ROOT. 2. Requiring the app's own vendor/autoload.php after the real server bootstrap lets the app's ClassLoader win the autoload race for any OCP class the server's classmap loader hasn't already loaded, shadowing real interfaces with the composer-dev-only nextcloud/ocp stubs - a hard fatal on the first file operation (OC\User\User::setEnabled() incompatible with the stub signature). The stub OCP\ mapping exists only for standalone unit tests; register IntegrationTestCase.php by hand instead and let PHPUnit's directory discovery load each *Test.php itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
40 tests across 6 classes (tests/run-integration.sh): - WopiProtocolTest: CheckFileInfo, GetFile including 206/416 range handling, PutFile lock and version conflict handling, the full lock protocol (LOCK/UNLOCK/REFRESH_LOCK/GET_LOCK/UnlockAndRelock), and RenameFile. - AdminSettingsTest: settings read/write via the FrontpageRoute endpoints. - EditorControllerTest, ShareGuestTokenTest: 401/404/415 branches and guest-token WOPI access issued directly via TokenManager. - CleanupJobTest: token/lock expiry purge and background-job registration. - RouteTest: the /index.php/-prefixed route form. run-integration.sh runs each class as its own phpunit process: in a single process, per-class recreation of the same test user leaves a stale mount-cache entry (likely LazyFolder/SetupManager) that makes later classes' file fixtures fail with NotPermittedException, while every class passes alone. One process per class sidesteps this at a small time cost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
ShareController::openShare() guarded its password challenge with $share->getPassword() !== ''. IShare::getPassword() returns string|null and is null (not '') for a share with no password, and null !== '' is true in PHP, so the guard fired for password-less shares too: any unauthenticated guest visiting a share link that was never password-protected got redirected to the generic Nextcloud share page instead of ever reaching the editor, and it masked every branch after it for unauthenticated requests, including the READ-permission check. Authenticated visitors were unaffected, which is why this went unnoticed. Updates ShareGuestTokenTest to assert the fixed behavior for a guest on a passwordless share, and re-targets the unreadable-share 403 test at an unauthenticated guest directly, since that branch is no longer masked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
handleRenameFile()'s path-traversal guard rejected names containing '/' or a null byte via basename() comparison, but basename() only treats '/' as a separator on non-Windows PHP builds, so a backslash-based name (e.g. "..\..\evil") passed through unmodified. Windows-separator names have no legitimate use in a rename, and NC storage treats '\' as an ordinary character rather than a path component - reject it explicitly alongside the existing checks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com> # Conflicts: # README.md # css/main-6N4mzMzV.chunk.css # css/office-main.css # js/office-main.mjs # js/office-main.mjs.license # js/office-main.mjs.map # lib/AppInfo/Application.php # vendor-bin/psalm/composer.json # vendor-bin/psalm/composer.lock
composer.json's autoload-dev still mapped OCP\ to vendor/nextcloud/ocp/OCP/, the path used before nextcloud/ocp was moved into its own isolated vendor-bin/nextcloud-ocp namespace (#128). That move never broke anything on its own branch since nothing there exercised the mapping, but merging in work that does (real OCP interfaces mocked in PHPUnit tests) surfaced it: every such test failed with "Class or interface ... does not exist". Repoints the mapping at the vendor-bin location and adds the psr/* sub-dependencies (event-dispatcher, http-message, http-client, container, log, clock) those stubs reference, which live in the same isolated tree. Also drops psalm.xml's now-unused UndefinedDocblockClass suppression for Doctrine\DBAL\Schema\* (nothing in lib/ references it) and regenerates psalm-baseline.xml, which had drifted two stale DeprecatedMethod entries for WopiMapper. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
|
/compile |
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
|
Found while running a live cross-replica EO test against this branch (two EO docservice replicas sharing Redis, real WOPI host round-trip): Out of scope for this PR (it's an NC36 rebase, not a fix), and pre-existing in A real fix would derive the allowed origin from |
|
Superseding this with #8 (feat/wopi-phase-5), which is now up to date with main directly - no need for a separate NC36-port branch. The CSP finding from the review comment above still stands and applies there too. |
Summary
Rebases the WOPI-host work from
feat/wopi-phase-1throughfeat/wopi-phase-5onto currentmain, which had diverged onto NC36 support and an unrelated set of overview/search/UI features while the WOPI branches sat unmerged.feat/wopi-phase-5(the most advanced of the five phase branches) ontomain. Real conflicts were minimal —lib/AppInfo/Application.php(two independent event-listener registrations, kept both) andREADME.md(both branches had added genuinely different sections; combined into one doc covering the overview app and the WOPI host together).mainmovednextcloud/ocpfrom a direct dependency into an isolatedvendor-binnamespace at some point after these branches diverged. That never broke anything onmainitself, since nothing there exercised the old autoload path — butphase-5's own PHPUnit tests mock realOCP\*interfaces, and they do. Fixed theautoload-devmapping in its own commit, separate from the merge.Doctrine\DBALsuppression, two stale baseline entries forWopiMapper).appinfo/info.xmlnow targets NC36 (main's current target), same as everything else in this repo — no NC-version-specific code changes were needed, sincemain's own NC36 migration (d16dfff) was a pure version-constraint bump with no OCP API changes.Why draft
This is not ready for a merge decision yet. Before this goes anywhere, we need to decide how WOPI-host support should actually live here — as a merged part of this app, or pulled out into its own standalone Nextcloud app, given it's a genuinely distinct concern from the overview/search UI
mainhas been evolving independently. Opening this now so the rebased-and-verified work isn't sitting only on a stale local branch while that gets decided.Test plan
npm ci && npm run build— compiles cleannpm run lint— cleannpm run test:unit— 120/120 JS tests passcomposer lint/composer cs:check— cleancomposer psalm— no errorscomposer test:unit— 107/107 PHP tests pass (0/107 before the autoload fix)tests/integration, in-container only) — not yet runAssisted-by: ClaudeCode:claude-sonnet-5