Commit Graph

4 Commits

Author SHA1 Message Date
Ignacio Nelson f12692520a Merge pull request #1694 from denkfabrik-li/fix/upload-folder-library-scope
Hold the folder an upload names to the same library boundary as everything else
2026-08-26 22:34:14 -03:00
denkfabrik-li 7727ad7616 Say what exists:folders,id was already being read as
Folder uses SoftDeletes. The `exists` rule runs against the table, so a
folder in the trash passes it -- while every resolution that follows goes
through Folder::query(), which honours the soft delete and finds nothing.
Ten rules across five controllers rely on that check, and each one reads
it as "this folder exists".

Two of them then wrote the id anyway. Api\FilesController::store()
resolves the folder, hands the null to Folder::uploadableBy(), is told
yes -- correctly, that is the rule for a root upload -- and passes
$validated['folder_id'] to the write. FilesController::store() is the
same shape once #1694 gives it the guard. FilesController::update() and
its API twin write it straight through with nothing in between.

The result is a live file inside a deleted folder, which is a state
nothing else in the application produces: FolderService::delete() deletes
every file in the subtree along with it. The row is reachable by id, in
search and over the API, and missing from the listing its uploader would
look in.

Rules::folderId() makes the check mean what its readers assume, once,
where the reasoning can be written down -- the same argument slug() makes
for itself one method above. Every site takes it, so the file cannot end
up with two spellings of the same rule and no way to tell which is the
safe one.

What changes, path by path:

  - POST /files, POST /api/v1/files, PATCH /files/{file} and
    PATCH /api/v1/files/{file} refuse a folder in the trash instead of
    writing its id. This is the fix.
  - POST /uploads used to accept it and quietly file the upload at the
    root -- its guard and its write already agreed, on null. It now says
    so instead, which is what the other upload paths do.
  - files/{file}/move, files/bulk-edit, folders, folders/{folder}/move
    and the portal's my-folders already refused, through
    StaffLibraryScope::folders() or Folder::scopeVisibleToClient(), both
    of which drop trashed rows. They still refuse; the answer is now 422
    naming folder_id rather than a bare 404. Those two guards are asking
    a different question -- "is this folder yours" -- and they keep
    asking it.

No live folder id behaves differently anywhere, and the root (a null
folder_id) is untouched.

The published API document is unchanged: `exists` renders the same either
way. Regenerated with php artisan scramble:export and byte-identical.
2026-08-26 06:01:06 +02:00
denkfabrik-li 2c2b86ffa1 Hold the folder an upload names to the same library boundary as everything else
Folder::uploadableBy() returned true for any staff member without looking
at the folder, on the strength of a comment saying staff had already
validated folder_id through FilesController's own flow. No upload path
did. FilesController::store() did not check the folder at all; the two
that called uploadableBy() — the API upload and the chunked upload the
browser actually posts to — called a guard that could only ever say yes.

A client-scoped staff member could therefore name any folder id and put
the file inside a subtree shared with somebody else's client, where
File::scopeVisibleToClient hands it over without an assignment row ever
being written. That is the boundary StaffLibraryScope's own docblock
claims to hold everywhere.

The staff branch now asks StaffLibraryScope::allowsFolder, which returns
true for unscoped staff, so nothing changes for them. The client branch
is untouched: a client is never client-scoped, and ownership or a public
folder opting into client uploads remains the whole of their rule.

The two folder pickers that fed those ids are narrowed the same way the
listings around them already are.
2026-08-26 04:01:25 +02:00
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00