mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-03 21:03:17 +00:00
a5b6538b31
Sharing a folder was four steps written in the web controller: the assignment row, the activity entry, the in-app notification and the digest email. The hosted edition's AI connector repeated them, because there was nothing in the core to call, and the two copies had already drifted (one re-notifies on a repeated share, the other does not). The folder API about to land would have been a third copy. FolderSharing is the folder twin of FileSharing, and the web controller now calls it. Behaviour on the web is unchanged. The count of files a staff member may not delete inside a folder's subtree moves out of FoldersController into UndeletableFiles, for the same reason: deleting a folder over the API has to ask exactly the question the web screen asks before the cascade takes files with it.
72 lines
2.7 KiB
PHP
72 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Folders;
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Files\Access\StaffLibraryScope;
|
|
use App\Modules\Files\Models\File;
|
|
use App\Modules\Files\Models\Folder;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
/**
|
|
* How many files in a folder's subtree a staff member may not delete.
|
|
*
|
|
* Deleting a folder cascades to every file in its subtree, and a File's
|
|
* `deleted` hook removes the bytes from disk — there is no restore.
|
|
* Authorizing the folder is not authorizing its contents: FilePolicy::delete
|
|
* asks for `delete_others_files` on somebody else's upload, and for the
|
|
* library boundary on top of that, and neither question is asked by
|
|
* FolderPolicy. Every staff path that deletes a folder asks this first, so
|
|
* the web screen and the API cannot disagree about what a cascade may take.
|
|
*
|
|
* Asked as one count rather than FilePolicy::delete per file: a folder can
|
|
* hold thousands, Gate resolves a fresh policy for every check, and a
|
|
* per-row policy check on a listing is the cost 0a8b609e went to some
|
|
* trouble to remove. The two halves of FilePolicy::delete are expressible
|
|
* in SQL — the permission half is constant for this viewer, and the
|
|
* library half is the query StaffLibraryScope already memoises per request.
|
|
*
|
|
* Somebody holding both delete permissions and no library scope can delete
|
|
* anything in the subtree by construction, so they never pay for the query
|
|
* at all.
|
|
*
|
|
* The client half of the same rule is MyFoldersController::destroy.
|
|
*/
|
|
class UndeletableFiles
|
|
{
|
|
public function __construct(
|
|
private readonly StaffLibraryScope $scope,
|
|
) {}
|
|
|
|
public function count(User $viewer, Folder $folder): int
|
|
{
|
|
$mayDeleteOwn = $viewer->can('delete_files');
|
|
$mayDeleteOthers = $viewer->can('delete_others_files');
|
|
$scoped = $viewer->isClientScoped();
|
|
|
|
if ($mayDeleteOwn && $mayDeleteOthers && ! $scoped) {
|
|
return 0;
|
|
}
|
|
|
|
return File::query()
|
|
->whereIn('folder_id', $folder->subtreeFolderIds())
|
|
->where(function (Builder $outer) use ($viewer, $mayDeleteOwn, $mayDeleteOthers, $scoped): void {
|
|
if (! $mayDeleteOwn) {
|
|
$outer->orWhere('uploaded_by', $viewer->id);
|
|
}
|
|
|
|
if (! $mayDeleteOthers) {
|
|
$outer->orWhere(fn (Builder $others): Builder => $others
|
|
->whereNull('uploaded_by')->orWhere('uploaded_by', '!=', $viewer->id));
|
|
}
|
|
|
|
if ($scoped) {
|
|
$outer->orWhereNotIn('id', $this->scope->files($viewer)->select('id'));
|
|
}
|
|
})
|
|
->count();
|
|
}
|
|
}
|