diff --git a/js/src/admin/extenders/customizeGetRequiredPermissions.tsx b/js/src/admin/extenders/customizeGetRequiredPermissions.tsx new file mode 100644 index 00000000..e03dce27 --- /dev/null +++ b/js/src/admin/extenders/customizeGetRequiredPermissions.tsx @@ -0,0 +1,16 @@ +import app from 'flarum/admin/app'; +import { override } from 'flarum/common/extend'; + +export default function customizeGetRequiredPermissions() { + override(app, 'getRequiredPermissions', (original, permission: string) => { + const required = original(permission) || []; + + if (permission === 'fof-upload.hideSharedUploads') { + if (!required.includes('fof-upload.download')) { + required.push('fof-upload.download'); + } + } + + return required; + }); +} diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts index ac84395c..c5311ef9 100644 --- a/js/src/admin/index.ts +++ b/js/src/admin/index.ts @@ -2,6 +2,7 @@ import app from 'flarum/admin/app'; import UploadPage from './components/UploadPage'; import extendAdminNav from './extendAdminNav'; import FileListState from '../common/states/FileListState'; +import customizeGetRequiredPermissions from './extenders/customizeGetRequiredPermissions'; export * from './components'; @@ -39,14 +40,59 @@ app.initializers.add('fof-upload', () => { 'moderate', 50 ) + .registerPermission( + { + icon: 'fas fa-eye-slash', + label: app.translator.trans('fof-upload.admin.permissions.hide_own_uploads_label'), + permission: 'fof-upload.hideUserUploads', + }, + 'reply', + 42 + ) + .registerPermission( + { + icon: 'fas fa-eye-slash', + label: app.translator.trans('fof-upload.admin.permissions.hide_uploads_of_others_label'), + permission: 'fof-upload.hideOtherUsersUploads', + }, + 'moderate', + 41 + ) + .registerPermission( + { + icon: 'fas fa-eye-slash', + label: app.translator.trans('fof-upload.admin.permissions.hide_shared_uploads_label'), + permission: 'fof-upload.hideSharedUploads', + }, + 'moderate', + 40 + ) .registerPermission( { icon: 'fas fa-trash', - label: app.translator.trans('fof-upload.admin.permissions.delete_uploads_of_others_label'), + label: app.translator.trans('fof-upload.admin.permissions.delete_own_uploads_label'), permission: 'fof-upload.deleteUserUploads', }, + 'reply', + 32 + ) + .registerPermission( + { + icon: 'fas fa-trash', + label: app.translator.trans('fof-upload.admin.permissions.delete_uploads_of_others_label'), + permission: 'fof-upload.deleteOtherUsersUploads', + }, 'moderate', - 50 + 31 + ) + .registerPermission( + { + icon: 'fas fa-trash', + label: app.translator.trans('fof-upload.admin.permissions.delete_shared_uploads_label'), + permission: 'fof-upload.deleteSharedUploads', + }, + 'moderate', + 30 ) .registerPermission( { @@ -66,6 +112,7 @@ app.initializers.add('fof-upload', () => { ); extendAdminNav(); + customizeGetRequiredPermissions(); //app.fileListState = new FileListState(); }); diff --git a/migrations/2025_11_07_000000_grant_hide_own_to_members.php b/migrations/2025_11_07_000000_grant_hide_own_to_members.php new file mode 100644 index 00000000..4f2df9f1 --- /dev/null +++ b/migrations/2025_11_07_000000_grant_hide_own_to_members.php @@ -0,0 +1,18 @@ + Group::MEMBER_ID, +]); diff --git a/migrations/2025_11_07_000001_migrate_permissions.php b/migrations/2025_11_07_000001_migrate_permissions.php new file mode 100644 index 00000000..5c76bd81 --- /dev/null +++ b/migrations/2025_11_07_000001_migrate_permissions.php @@ -0,0 +1,45 @@ + function (Builder $schema) { + $db = $schema->getConnection(); + + $groups = $db->table('group_permission') + ->where('permission', 'fof-upload.deleteUserUploads') + ->pluck('group_id'); + + foreach ($groups as $gid) { + foreach ([ + 'fof-upload.deleteOtherUsersUploads', + 'fof-upload.hideOtherUsersUploads', + ] as $perm) { + $db->table('group_permission')->updateOrInsert( + ['group_id' => $gid, 'permission' => $perm], + [] + ); + } + } + }, + + 'down' => function (Builder $schema) { + $schema->getConnection() + ->table('group_permission') + ->whereIn('permission', [ + 'fof-upload.deleteOtherUsersUploads', + 'fof-upload.hideOtherUsersUploads', + ]) + ->delete(); + }, +]; diff --git a/resources/locale/en.yml b/resources/locale/en.yml index 31df51dd..0a5bd057 100644 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -112,7 +112,12 @@ fof-upload: download_label: Download files upload_label: Upload files view_user_uploads_label: View user uploads - delete_uploads_of_others_label: Delete user uploads + hide_own_uploads_label: Hide own uploads + hide_uploads_of_others_label: Hide uploads of other users + hide_shared_uploads_label: Hide shared uploads + delete_own_uploads_label: Delete own uploads + delete_uploads_of_others_label: Delete uploads of other users + delete_shared_uploads_label: Delete shared uploads upload_shared_label: Upload shared files access_shared_label: Access shared files in Media Manager templates: diff --git a/src/Access/FilePolicy.php b/src/Access/FilePolicy.php index 90033d9a..51a9727c 100644 --- a/src/Access/FilePolicy.php +++ b/src/Access/FilePolicy.php @@ -15,9 +15,15 @@ use Flarum\User\Access\AbstractPolicy; use Flarum\User\User; use FoF\Upload\File; +use FoF\Upload\Helpers\Util; class FilePolicy extends AbstractPolicy { + public function __construct( + protected Util $util + ) { + } + public function viewInfo(User $actor, File $file) { // for now.. @@ -26,15 +32,31 @@ public function viewInfo(User $actor, File $file) public function hide(User $actor, File $file) { - if (($file->actor?->id === $actor->id || $actor->hasPermission('fof-upload.deleteUserUploads')) && $file->actor !== null) { - return $this->allow(); + $isShared = $this->util->isPrivateShared($file) || $file->shared; + + if ($isShared) { + return $actor->can('fof-upload.hideSharedUploads') ? $this->allow() : $this->deny(); + } + + if ($file->actor_id === $actor->id) { + return $actor->can('fof-upload.hideUserUploads') ? $this->allow() : $this->deny(); } + + return $actor->can('fof-upload.hideOtherUsersUploads') ? $this->allow() : $this->deny(); } public function delete(User $actor, File $file) { - if ($actor->can('fof-upload.deleteUserUploads') && $file->actor !== null) { - return $this->allow(); + $isShared = $this->util->isPrivateShared($file) || $file->shared; + + if ($isShared) { + return $actor->can('fof-upload.deleteSharedUploads') ? $this->allow() : $this->deny(); } + + if ($file->actor_id === $actor->id) { + return $actor->can('fof-upload.deleteUserUploads') ? $this->allow() : $this->deny(); + } + + return $actor->can('fof-upload.deleteOtherUsersUploads') ? $this->allow() : $this->deny(); } } diff --git a/src/Commands/DeleteFileHandler.php b/src/Commands/DeleteFileHandler.php index 2fa0ae5e..b262dbda 100644 --- a/src/Commands/DeleteFileHandler.php +++ b/src/Commands/DeleteFileHandler.php @@ -14,6 +14,7 @@ use Flarum\Foundation\ValidationException; use Flarum\Settings\SettingsRepositoryInterface; +use Flarum\User\Exception\PermissionDeniedException; use FoF\Upload\Adapters\Manager; use FoF\Upload\File; use FoF\Upload\Helpers\Util; @@ -42,30 +43,21 @@ public function __construct( public function handle(DeleteFile $command): void { - $privateShared = $this->util->isPrivateShared($command->file); - - if ($privateShared || $command->file->shared) { - $command->actor->assertCan('fof-upload.upload-shared-files'); - } else { - // We don't currently have a permission for this, so we'll just use admin. - $command->actor->assertAdmin(); + if (!$command->actor->can('delete', $command->file)) { + throw new PermissionDeniedException(); } - $success = false; + $privateShared = $this->util->isPrivateShared($command->file); - // Delete the file from storage. - if ($privateShared) { - $success = $this->deleteSharedFile($command->file); - } else { - $success = $this->deleteFileViaAdaptor($command->file); - } + $success = $privateShared + ? $this->deleteSharedFile($command->file) + : $this->deleteFileViaAdaptor($command->file); if ($success === false) { throw new ValidationException(['file' => 'Could not delete file.']); - } else { - // Delete the file record from the database. - $command->file->delete(); } + + $command->file->delete(); } protected function deleteSharedFile(File $file): bool diff --git a/tests/integration/api/HideFilesTest.php b/tests/integration/api/HideFilesTest.php index 701a6da1..fb9bb358 100644 --- a/tests/integration/api/HideFilesTest.php +++ b/tests/integration/api/HideFilesTest.php @@ -14,6 +14,7 @@ use Flarum\Extend; use Flarum\Foundation\Paths; +use Flarum\Group\Group; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use FoF\Upload\File; use FoF\Upload\Tests\EnhancedTestCase; @@ -32,19 +33,29 @@ public function setUp(): void $this->prepareDatabase([ 'users' => [ $this->normalUser(), - ['id' => 3, 'username' => 'normal2', 'email' => 'normal2@machine.local'], - ['id' => 4, 'username' => 'moderator', 'email' => 'moderator@machine.local'], + ['id' => 3, 'username' => 'normal2', 'email' => 'normal2@machine.local', 'is_email_confirmed' => 1], + ['id' => 4, 'username' => 'moderator', 'email' => 'moderator@machine.local', 'is_email_confirmed' => 1], ], 'fof_upload_files' => [ ['id' => 1, 'base_name' => 'test_file.abc', 'uuid' => 'abc-123', 'path' => 'path/test_file.abc', 'url' => 'http://localhost/test_file.abc', 'type' => 'test/file', 'size' => 123, 'upload_method' => 'local', 'actor_id' => 2, 'shared' => false], ['id' => 2, 'base_name' => 'test_file2.abc', 'uuid' => 'def-456', 'path' => 'path/test_file2.abc', 'url' => 'http://localhost/test_file2.abc', 'type' => 'test/file', 'size' => 123, 'upload_method' => 'local', 'shared' => true], ], 'group_user' => [ - ['user_id' => 4, 'group_id' => 4], + ['user_id' => 4, 'group_id' => Group::MODERATOR_ID], ], 'group_permission' => [ - ['group_id' => 4, 'permission' => 'fof-upload.deleteUserUploads'], - ['group_id' => 4, 'permission' => 'fof-upload.viewUserUploads'], + // General permissions + ['group_id' => Group::MEMBER_ID, 'permission' => 'fof-upload.download'], + ['group_id' => Group::MEMBER_ID, 'permission' => 'fof-upload.viewUserUploads'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'fof-upload.download'], + + // Hiding permissions + ['group_id' => Group::MEMBER_ID, 'permission' => 'fof-upload.hideUserUploads'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'fof-upload.hideOtherUsersUploads'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'fof-upload.hideSharedUploads'], + + // Deletion permissions + ['group_id' => Group::MODERATOR_ID, 'permission' => 'fof-upload.deleteUserUploads'], ], ]); } @@ -248,9 +259,9 @@ public function admin_can_hide_shared_files() /** * @test */ - public function moderator_cannot_hide_shared_files() + public function moderator_can_hide_shared_files() { - $uuid = 'def-456'; + $uuid = $this->uploadSharedFileAndGetUuid(); $response = $this->send( $this->request( @@ -264,8 +275,7 @@ public function moderator_cannot_hide_shared_files() ] ) ); - - $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode()); } /** diff --git a/tests/integration/api/SharedFilesTest.php b/tests/integration/api/SharedFilesTest.php index 220e3b3d..4b03fb58 100644 --- a/tests/integration/api/SharedFilesTest.php +++ b/tests/integration/api/SharedFilesTest.php @@ -12,6 +12,7 @@ namespace FoF\Upload\Tests\integration\api; +use Flarum\Group\Group; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use FoF\Upload\File; use FoF\Upload\Tests\EnhancedTestCase; @@ -36,7 +37,8 @@ public function setUp(): void ['user_id' => 3, 'group_id' => 4], ], 'group_permission' => [ - ['permission' => 'fof-upload.upload-shared-files', 'group_id' => 4], + ['permission' => 'fof-upload.upload-shared-files', 'group_id' => Group::MODERATOR_ID], + ['permission' => 'fof-upload.deleteSharedUploads', 'group_id' => Group::MODERATOR_ID], ], ]); }