fix: accept string node ids from the Files sidebar when requesting a signature - #8367
fix: accept string node ids from the Files sidebar when requesting a signature#8367maia-andre wants to merge 2 commits into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 220 files with indirect coverage changes 🚀 New features to boost your workflow:
|
vitormattos
left a comment
There was a problem hiding this comment.
I think we need to follow the ID value through the complete flow before normalizing nodeId, fileId, and id with the same helper.
LibreSign already completed the migration to the current meaning where:
nodeId/signedNodeIdidentify Nextcloud nodes;id/fileId/parentFileIdidentify records inlibresign_file.
If any current code still uses fileId for a Nextcloud node ID, that should be treated as a leftover from the old model and corrected, not as a valid alternative meaning.
We already found at least one example of this kind of leftover: RequestSignatureService still has a local $fileId whose value comes from Node::getId() or file.nodeId, and it is then passed to getByNodeId(). In other places, fileId correctly goes to getById().
Because of this history, I do not think we should infer what an ID means only from its property name. We need to follow the actual value.
Looking more about this, I found an important Nextcloud change to consider. In the current @nextcloud/files API, Node.id is string | undefined. Node.fileid is the legacy numeric property and is deprecated. The string representation is intentional because Nextcloud is moving to 64-bit snowflake IDs, which cannot always be represented safely as JavaScript numbers.
Nextcloud documents Snowflake IDs here:
https://docs.nextcloud.com/server/stable/developer_manual/digging_deeper/snowflake_ids.html
They were added in Nextcloud 33 and are 64-bit identifiers. The Nextcloud 33 developer release notes also mention that APIs migrated to Snowflake IDs use strings instead of integers:
https://docs.nextcloud.com/server/stable/developer_manual/release_notes/previous/upgrade_to_33.html
Could we first trace the exact #8363 flow and document where the value comes from and what it represents at each step?
For example:
Nextcloud Node -> tab.ts -> AppFilesTab -> files store -> serializeRequestFile() -> request-signature API -> backend lookup
For each value used as nodeId, fileId, or id in this flow, please verify:
- where the value originates;
- whether it identifies a Nextcloud node or a
libresign_filerow; - whether it is renamed or transformed on the way;
- which backend lookup finally consumes it (
getByNodeId(),getById(), NextcloudgetById(), etc.).
If this flow still uses fileId for a Nextcloud node ID anywhere, that should be corrected as part of the leftover cleanup from the completed migration.
The fix should happen at the point where the representation first becomes incorrect.
In particular, converting a Nextcloud Node.id string with Number() is not safe for future snowflake IDs. A value above Number.MAX_SAFE_INTEGER can silently become a different ID.
I would therefore avoid making serializeRequestFile() generally accept and convert numeric strings until we know which representations are valid for each field.
Please also avoid using an artificial state such as nodeId: 'temp-node' to define the domain model unless production code can really produce that value. The regression tests should reproduce the real sidebar data path as closely as possible.
While following this flow, please also check the test coverage of every method or branch that needs to be changed. If the relevant behavior is not already covered, please add a focused test before or together with the change. The tests should protect the real ID semantics and the complete regression path, not only the final serializer output.
This PR does not need to audit every ID in LibreSign. It should trace and fix the complete #8363 path. If that investigation exposes other leftovers from the old fileId = Nextcloud node ID model, we can handle those in a separate cleanup issue.
|
Thanks — agreed on all three points, and the trace changed my view of where the fix belongs. Below is the #8363 path on Where the string comes from
Then, in The path
Where the fix belongsTwo places make the representation incorrect, and neither should convert with
One decision I need from you — the API contract.
I lean to (a); it is the honest description of what the endpoint accepts, and it is one line plus generated files. Tests (real sidebar data, no
|
|
Thanks, this trace is much clearer, and the proposed direction looks consistent with what I found as well. I checked the flow against the current LibreSign code and the current Nextcloud contracts. The important distinction seems to be:
So the flow that makes the most sense for LibreSign is:
This also means converting the value with I would also treat the Your trace of the LibreSign path also looks correct to me:
I think the typing is especially important here. On the frontend, the type should reflect the real On the backend, I would prefer to validate and normalize the HTTP value once at a clear boundary, then let strong The intended model would be:
If the current data structure makes it difficult to propagate the normalized value, a small shared normalizer would be preferable to repeated casts in different methods. Because of that, I think the safer frontend change would be to keep the fix specific to For the API input, I think we can make the decision here: Could you also check what OpenAPI is actually generated from the proposed Internally, PHP and database values can remain I would keep changing The test plan looks good. Using a real node ID above I also agree with leaving the other leftovers out of this PR. Since some old For backports, I would check affectedness rather than only whether the patch cherry-picks cleanly.
With that, the direction of the rewrite looks good to me. |
`LibresignNewFile.nodeId` was declared as an integer only, but the Files app exposes node ids as strings (`Node.id` of `@nextcloud/files`) and, since Nextcloud 33, they can exceed what a JavaScript number holds, so the client cannot safely convert them. A string reached the services raw: `RequestSignatureService::saveFile()` swallowed the TypeError of `FileMapper::getByNodeId(int)` and `FileService::getNodeFromData()` propagated the one of `FolderService::getFileByNodeId(int)` as a 422. Validate and normalize the value once, at the boundary between the HTTP payload and the services, and keep `int` from there on: - `FileInputValidator::normalizeNodeId()` turns a string of digits into an int (PHP integers are 64-bit, so node ids are safe there) and leaves anything else untouched for `validateFile()` to reject as before. - `RequestSignatureWorkflowService::createRequest()` and `updateExistingRequest()` apply it to `file` and to each entry of `files` before building the payload the services receive, so the `nodeId?: int` contract of `saveFile()` holds without further casts. - The local `$fileId` of `saveFile()` is renamed to `$nodeId`: it always held a Nextcloud node id. - `LibresignNewFile.nodeId` is declared as `non-negative-int|numeric-string` (generated schema: `oneOf` integer/string; TypeScript: `number | string`) and the parameter description states the accepted forms. Responses are unchanged. Tests cover the string and the integer forms on each changed method, including a node id above `Number.MAX_SAFE_INTEGER`, and the `nodeId` branch of `saveFile()`, which had no coverage. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
…gnature `tab.ts` builds `fileInfo.id` from `node.fileid ?? node.id`. With `@nextcloud/files`, `fileid` is the deprecated numeric getter and is `undefined` when the id does not fit a JavaScript number, while `id` is always a string; a file copied in the Files app and opened in the sidebar hits this path too. `AppFilesTab.update()` stored that string as `nodeId` unchanged, but `serializeRequestFile()` only accepted numbers, so the request went out without `file` and the API answered 422 "File or files parameter is required". Keep `nodeId` in the string domain instead of converting it: the serializer forwards a positive integer or a string of digits as it is, and the API accepts both. `fileId` and `id` only ever come from the LibreSign API as numbers and are not touched. The `FileInfo.id` type of `AppFilesTab` now matches what `tab.ts` sends. Tests follow the sidebar path with a node id above `Number.MAX_SAFE_INTEGER`: `tab.ts` keeps the string, `AppFilesTab` passes it through, and the store sends it unchanged for a single file and for envelope files; the empty id `tab.ts` falls back to is still not sent. Resolves: LibreSign#8363 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
9a5292e to
86e816a
Compare
|
Rewritten along the lines you described — the description is updated with the new shape, the generated schema and the manual verification. Short version of the points you asked me to check:
One open point, in the description: |
vitormattos
left a comment
There was a problem hiding this comment.
The new direction looks good to me. I think there are only two points left to clarify before this is ready:
-
LibresignNewFileis shared by other endpoints. IfnodeIdnow acceptsinteger | string, could you check the other consumers and make sure they also support that input? Otherwise, it may be better to keep this wider input type specific to the request-signature API. -
Since
normalizeNodeId()is the boundary where the HTTP value becomes a PHPint, I think it would be safer if it either returns a validintor rejects the value. Leaving an invalid numeric string untouched can still allow later code to cast it differently.
Other than that, the rewrite looks aligned with the #8363 flow and the test coverage is much better now.
Resolves: #8363
📝 Summary
Rewritten after the trace in the comments (previous approach — a generic
Number()helper fornodeId/fileId/id— dropped).@nextcloud/filesexposesNode.idas a string;Node.fileidis the deprecated numeric getter and isundefinedwhen the id does not fit a JavaScript number (Nextcloud ≥ 33).tab.tssends that string toAppFilesTab, which stores it asnodeId, andserializeRequestFile()only accepted numbers — the request went out withoutfileand the API answered 422 File or files parameter is required. Had the string been forwarded, the backend would have failed too:saveFile()swallowed theTypeErrorofgetByNodeId(int)andgetNodeFromData()propagated the one ofgetFileByNodeId(int).The model is the one agreed above —
Node.id (string) → HTTP payload (int | decimal string) → validate/normalize once at the PHP boundary → int → OCP Files API:Backend (first commit)
FileInputValidator::normalizeNodeId()turns a string of digits into anint(PHP integers are 64-bit) and leaves any other value untouched forvalidateFile()to reject as it already does.RequestSignatureWorkflowService::createRequest()/updateExistingRequest()apply it once tofileand to each entry offiles, before the payload reaches the services; thenodeId?: intcontract ofsaveFile()then holds without casts in the services.saveFile(): the local$fileIdthat held a node id is renamed$nodeId.LibresignNewFile.nodeIdis declarednon-negative-int|numeric-string; the parameter description says "an integer or a string of digits". Responses are unchanged.Frontend (second commit) — specific to
nodeId, noNumber()serializeRequestFile()forwardsnodeIdwhen it is a positive integer or a string of digits, as it is.fileId/idkeep the number-only checks.AppFilesTab'sFileInfo.idbecomesnumber | string, matching whattab.tssends.About the generated OpenAPI
openapi-extractormapsnumeric-stringto a plainstringand has nopatternsupport, so the generated schema isand the TypeScript type is
nodeId?: number | string. The "digits only" part lives in the parameter description and in the validator (422 Invalid fileID for anything else). If you would rather have a stricter schema, I can hand-write it, but it would be lost at the nextcomposer openapi.LibresignNewFileis also the input ofPOST /fileandPOST /id-docs. I kept the normalizer call to the request-signature path, as agreed; those two still passnodeIdraw togetNodeFromData()/saveFile(), so a string there yields the sameTypeError422 as before this PR (no regression). It is the same one-line call at each boundary — tell me if you want them here or in the leftovers issue.🧪 How to test
Unit tests use
9007199254740993(Number.MAX_SAFE_INTEGER + 2) and keep the integer path on each changed method:FileInputValidatorTest::testNormalizeNodeId(int, digits, 64-bit, abovePHP_INT_MAX, non-numeric, signed, empty, absent);RequestSignatureWorkflowServiceTest— string normalized once forfile, integer kept, each entry offiles,updateExistingRequest;RequestSignatureServiceTest::testSaveFileReusesTheFileRegisteredForTheNodeId(thenodeIdbranch ofsaveFile()had no coverage).tab.spec.tskeeps the string id of a node withoutfileid;AppFilesTab.spec.tspasses it through;files.spec.tssends it unchanged for a single file and for envelope files (two of these fail onmain), and the empty idtab.tsfalls back to is still not sent.Local checks: Vitest 127/127 on the three specs, ESLint and
vue-tscclean; php-cs-fixer clean; psalm on the changed files: only the pre-existingMissingDependencyinRequestSignatureService; PHPUnit for the four touched PHP classes: OK.Manual verification (devcontainer, Nextcloud 36 dev)
A real node id above
Number.MAX_SAFE_INTEGER: PDF uploaded via WebDAV, itsfileidset to9007199254740993inoc_filecache(PROPFIND returns it), then Files → Details → LibreSign tab → Add signer.window.OCA.Libresign.fileInfo.idis the string"9007199254740993"(fileidisundefinedon that node).Before (
mainbundle): saving the signer sendsPATCH /request-signaturewith"file": null→ 422 — the message from the issue:After: the same step sends
"file": {"nodeId": "9007199254740993"}→ 200; thelibresign_filerow hasnode_id = 9007199254740993(BIGINT), and Request signatures → Send completes:Two things seen on the way, not touched here: the Files app itself logs Failed to open sidebar on file 9007199254740992 (core,
fileidcoerced to a number), and the placeholder-fileInfo.idproduces aGET /file/validate/file_id/-9007199254740992404 (the unary-minus leftover already listed).⚙️ API / Back‑end changes
LibresignNewFile.nodeIdacceptsinteger | string of digits(request input only); generatedopenapi*.jsonandsrc/types/openapi/*.tsupdatedFileInputValidator::normalizeNodeId(array $file): array🚧 Backport
Affected:
stable34(reported) andstable33(string node ids start there); notstable32. The frontend commit cherry-picks cleanly on both; the backend one does not (noRequestSignatureWorkflowService/FileInputValidatorthere — the boundary is the controller and the validator isValidateHelper). After merge I will open manual backports forstable33andstable34rather than/backport.Leftovers for a separate issue (unchanged, as agreed)
openInLibreSignAction.js:77(fileIdcarrying a node id),AppFilesTab.vueparseIntonhandleNodeDeleted,showStatusInlineAction.js:12,SignFileService.php:143,getFileIdByNodeId()strict===and the-fileInfo.idplaceholder key,getSelectedFileView()dropping a stringnodeId, and the two otherLibresignNewFileconsumers above.✅ Checklist
🤖 AI (if applicable)