diff --git a/extensions/audit/extend.php b/extensions/audit/extend.php index fd726dbeb0..f7d1455551 100644 --- a/extensions/audit/extend.php +++ b/extensions/audit/extend.php @@ -33,170 +33,165 @@ Search\AuditGambits::register('ip', 'ip:', 'flarum-audit.lib.browser.filters.ip'); Search\AuditGambits::register('discussion', 'discussion:', 'flarum-audit.lib.browser.filters.discussion'); -return array_merge( - [ - (new Extend\Frontend('forum')) - ->js(__DIR__.'/js/dist/forum.js') - ->jsDirectory(__DIR__.'/js/dist/forum') - ->css(__DIR__.'/less/forum.less'), - - (new Extend\Frontend('common')) - ->jsDirectory(__DIR__.'/js/dist/common'), - - (new Extend\Frontend('admin')) - ->js(__DIR__.'/js/dist/admin.js') - ->jsDirectory(__DIR__.'/js/dist/admin') - ->css(__DIR__.'/less/admin.less') - ->content(Content\AdminPayload::class), - - new Extend\Locales(__DIR__.'/locale'), - - (new Extend\Middleware('forum')) - ->add(Middleware\SetLoggerActor::class), - (new Extend\Middleware('admin')) - ->add(Middleware\SetLoggerActor::class), - (new Extend\Middleware('api')) - ->add(Middleware\SetLoggerActor::class) - ->add(Middleware\ExtendSetPermissionController::class) - ->add(Middleware\LogPasswordResetAttempt::class), - - // Core integrations. - - (new Audit()) - ->group(null) - ->register('cache_cleared') - ->listen(ClearingCache::class, 'cache_cleared', function () { - return []; - }), - - (new Audit()) - ->group(null) - ->register('extension.disabled', 'extension.enabled', 'extension.uninstalled') - ->listen(ExtensionEvent\Disabled::class, 'extension.disabled', function ($e) { - return ['package' => $e->extension->name]; - }) - ->listen(ExtensionEvent\Enabled::class, 'extension.enabled', function ($e) { - return ['package' => $e->extension->name]; - }) - ->listen(ExtensionEvent\Uninstalled::class, 'extension.uninstalled', function ($e) { - return ['package' => $e->extension->name]; - }), - - (new Audit()) - ->group(null) - ->register('discussion.created', 'discussion.deleted', 'discussion.hidden', 'discussion.renamed', 'discussion.restored') - ->listen(DiscussionEvent\Started::class, 'discussion.created', function ($e) { - return ['discussion_id' => $e->discussion->id]; - }) - ->listen(DiscussionEvent\Deleted::class, 'discussion.deleted', function ($e) { - return ['discussion_id' => $e->discussion->id]; - }) - ->listen(DiscussionEvent\Hidden::class, 'discussion.hidden', function ($e) { - return ['discussion_id' => $e->discussion->id]; - }) - ->listen(DiscussionEvent\Restored::class, 'discussion.restored', function ($e) { - return ['discussion_id' => $e->discussion->id]; - }) - ->listen(DiscussionEvent\Renamed::class, 'discussion.renamed', function ($e) { - return [ - 'discussion_id' => $e->discussion->id, - 'old_title' => $e->oldTitle, - 'new_title' => $e->discussion->title, - ]; - }), - - (new Audit()) - ->group(null) - ->register('post.created', 'post.deleted', 'post.hidden', 'post.restored', 'post.revised') - ->listen(PostEvent\Deleted::class, 'post.deleted', function ($e) { - return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; - }) - ->listen(PostEvent\Hidden::class, 'post.hidden', function ($e) { - return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; - }) - ->listen(PostEvent\Restored::class, 'post.restored', function ($e) { - return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; - }) - ->listen(PostEvent\Revised::class, 'post.revised', function ($e) { - return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; - }) - // Not logging the first post. There's always going to be one created alongside the discussion. - ->listen(PostEvent\Posted::class, 'post.created', function ($e) { - return $e->post->number === 1 ? null : ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; - }), - - (new Audit()) - ->group(null) - // permission_changed and password_reset_attempted are logged from middleware; - // setting_changed and the user.* actions come from the integrations below. - ->register('permission_changed', 'setting_changed', 'user.password_reset_attempted') - ->using(new Integration\CoreSettingIntegration()) - ->using(new Integration\CoreUserIntegration()), - - (new Audit()) - ->group(null) - ->register('group.created', 'group.renamed', 'group.deleted') - // Group CRUD is request-scoped, so the acting admin is the ambient logger actor - // (the events themselves carry no actor — same as the discussion/post listeners above). - ->listen(GroupEvent\Created::class, 'group.created', function ($e) { - return ['group_id' => $e->group->id, 'name' => $e->group->name_singular]; - }) - ->listen(GroupEvent\Renamed::class, 'group.renamed', function ($e) { - return [ - 'group_id' => $e->group->id, - 'old_name' => $e->oldNameSingular, - 'new_name' => $e->group->name_singular, - ]; - }) - ->listen(GroupEvent\Deleted::class, 'group.deleted', function ($e) { - return ['group_id' => $e->group->id, 'name' => $e->group->name_singular]; - }), - - (new Audit()) - ->group(null) - ->register('developer_token_created') - // Logs the issuance of a long-lived developer API token. The raw token value is - // deliberately never logged — only the owner and the human-readable title. - ->listen(DeveloperTokenCreated::class, 'developer_token_created', function ($e) { - return ['user_id' => $e->token->user_id, 'title' => $e->token->title]; - }), - - (new Audit()) - ->group(null) - ->register('settings_reset') - ->listen(SettingsEvent\Reset::class, 'settings_reset', function ($e) { - return ['extension' => $e->extensionId, 'keys' => $e->keys]; - }), - - // Search. - - (new Extend\SearchDriver(DatabaseSearchDriver::class)) - ->addSearcher(AuditLog::class, AuditSearcher::class) - ->setFulltext(AuditSearcher::class, Search\FulltextFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\ActionFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\ActorFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\ClientFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\DiscussionFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\IpFilter::class) - ->addFilter(AuditSearcher::class, Search\Filter\UserFilter::class), - - (new Extend\Console()) - ->command(Console\ClearLogsCommand::class), - - new Extend\ApiResource(Api\Resource\AuditLogResource::class), - - (new Extend\ApiResource(ForumResource::class)) - ->fields(ForumAttributes::class), - - (new Extend\ServiceProvider()) - ->register(LoggerServiceProvider::class), - - (new Extend\ModelVisibility(AuditLog::class)) - ->scope(Scope\View::class), - - new LogSelfEnabled(), - ], - // Audit integrations for third-party extensions live in a separate file because they - // reference classes from extensions outside the Flarum monorepo. See the file header. - require __DIR__.'/extend.thirdparty.php' -); +return [ + (new Extend\Frontend('forum')) + ->js(__DIR__.'/js/dist/forum.js') + ->jsDirectory(__DIR__.'/js/dist/forum') + ->css(__DIR__.'/less/forum.less'), + + (new Extend\Frontend('common')) + ->jsDirectory(__DIR__.'/js/dist/common'), + + (new Extend\Frontend('admin')) + ->js(__DIR__.'/js/dist/admin.js') + ->jsDirectory(__DIR__.'/js/dist/admin') + ->css(__DIR__.'/less/admin.less') + ->content(Content\AdminPayload::class), + + new Extend\Locales(__DIR__.'/locale'), + + (new Extend\Middleware('forum')) + ->add(Middleware\SetLoggerActor::class), + (new Extend\Middleware('admin')) + ->add(Middleware\SetLoggerActor::class), + (new Extend\Middleware('api')) + ->add(Middleware\SetLoggerActor::class) + ->add(Middleware\ExtendSetPermissionController::class) + ->add(Middleware\LogPasswordResetAttempt::class), + + // Core integrations. + + (new Audit()) + ->group(null) + ->register('cache_cleared') + ->listen(ClearingCache::class, 'cache_cleared', function () { + return []; + }), + + (new Audit()) + ->group(null) + ->register('extension.disabled', 'extension.enabled', 'extension.uninstalled') + ->listen(ExtensionEvent\Disabled::class, 'extension.disabled', function ($e) { + return ['package' => $e->extension->name]; + }) + ->listen(ExtensionEvent\Enabled::class, 'extension.enabled', function ($e) { + return ['package' => $e->extension->name]; + }) + ->listen(ExtensionEvent\Uninstalled::class, 'extension.uninstalled', function ($e) { + return ['package' => $e->extension->name]; + }), + + (new Audit()) + ->group(null) + ->register('discussion.created', 'discussion.deleted', 'discussion.hidden', 'discussion.renamed', 'discussion.restored') + ->listen(DiscussionEvent\Started::class, 'discussion.created', function ($e) { + return ['discussion_id' => $e->discussion->id]; + }) + ->listen(DiscussionEvent\Deleted::class, 'discussion.deleted', function ($e) { + return ['discussion_id' => $e->discussion->id]; + }) + ->listen(DiscussionEvent\Hidden::class, 'discussion.hidden', function ($e) { + return ['discussion_id' => $e->discussion->id]; + }) + ->listen(DiscussionEvent\Restored::class, 'discussion.restored', function ($e) { + return ['discussion_id' => $e->discussion->id]; + }) + ->listen(DiscussionEvent\Renamed::class, 'discussion.renamed', function ($e) { + return [ + 'discussion_id' => $e->discussion->id, + 'old_title' => $e->oldTitle, + 'new_title' => $e->discussion->title, + ]; + }), + + (new Audit()) + ->group(null) + ->register('post.created', 'post.deleted', 'post.hidden', 'post.restored', 'post.revised') + ->listen(PostEvent\Deleted::class, 'post.deleted', function ($e) { + return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; + }) + ->listen(PostEvent\Hidden::class, 'post.hidden', function ($e) { + return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; + }) + ->listen(PostEvent\Restored::class, 'post.restored', function ($e) { + return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; + }) + ->listen(PostEvent\Revised::class, 'post.revised', function ($e) { + return ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; + }) + // Not logging the first post. There's always going to be one created alongside the discussion. + ->listen(PostEvent\Posted::class, 'post.created', function ($e) { + return $e->post->number === 1 ? null : ['discussion_id' => $e->post->discussion->id, 'post_id' => $e->post->id]; + }), + + (new Audit()) + ->group(null) + // permission_changed and password_reset_attempted are logged from middleware; + // setting_changed and the user.* actions come from the integrations below. + ->register('permission_changed', 'setting_changed', 'user.password_reset_attempted') + ->using(new Integration\CoreSettingIntegration()) + ->using(new Integration\CoreUserIntegration()), + + (new Audit()) + ->group(null) + ->register('group.created', 'group.renamed', 'group.deleted') + // Group CRUD is request-scoped, so the acting admin is the ambient logger actor + // (the events themselves carry no actor — same as the discussion/post listeners above). + ->listen(GroupEvent\Created::class, 'group.created', function ($e) { + return ['group_id' => $e->group->id, 'name' => $e->group->name_singular]; + }) + ->listen(GroupEvent\Renamed::class, 'group.renamed', function ($e) { + return [ + 'group_id' => $e->group->id, + 'old_name' => $e->oldNameSingular, + 'new_name' => $e->group->name_singular, + ]; + }) + ->listen(GroupEvent\Deleted::class, 'group.deleted', function ($e) { + return ['group_id' => $e->group->id, 'name' => $e->group->name_singular]; + }), + + (new Audit()) + ->group(null) + ->register('developer_token_created') + // Logs the issuance of a long-lived developer API token. The raw token value is + // deliberately never logged — only the owner and the human-readable title. + ->listen(DeveloperTokenCreated::class, 'developer_token_created', function ($e) { + return ['user_id' => $e->token->user_id, 'title' => $e->token->title]; + }), + + (new Audit()) + ->group(null) + ->register('settings_reset') + ->listen(SettingsEvent\Reset::class, 'settings_reset', function ($e) { + return ['extension' => $e->extensionId, 'keys' => $e->keys]; + }), + + // Search. + + (new Extend\SearchDriver(DatabaseSearchDriver::class)) + ->addSearcher(AuditLog::class, AuditSearcher::class) + ->setFulltext(AuditSearcher::class, Search\FulltextFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\ActionFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\ActorFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\ClientFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\DiscussionFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\IpFilter::class) + ->addFilter(AuditSearcher::class, Search\Filter\UserFilter::class), + + (new Extend\Console()) + ->command(Console\ClearLogsCommand::class), + + new Extend\ApiResource(Api\Resource\AuditLogResource::class), + + (new Extend\ApiResource(ForumResource::class)) + ->fields(ForumAttributes::class), + + (new Extend\ServiceProvider()) + ->register(LoggerServiceProvider::class), + + (new Extend\ModelVisibility(AuditLog::class)) + ->scope(Scope\View::class), + + new LogSelfEnabled(), +]; diff --git a/extensions/audit/extend.thirdparty.php b/extensions/audit/extend.thirdparty.php deleted file mode 100644 index 1d09e33e7f..0000000000 --- a/extensions/audit/extend.thirdparty.php +++ /dev/null @@ -1,130 +0,0 @@ -whenExtensionEnabled('flarum-audit', ...). Once an - * extension adopts the public API, its block here can be removed. - */ -return [ - (new Conditional()) - ->whenExtensionEnabled('fof-ban-ips', function () { - return [ - (new Audit()) - ->group('fof-ban-ips') - ->listen(BanIPsEvents\IPWasBanned::class, 'fof_ban_ips.banned', function ($e) { - return array_filter([ - 'ip' => $e->bannedIP->address, - 'reason' => $e->bannedIP->reason, - 'user_id' => $e->bannedIP->user_id ?: null, - ], function ($v) { - return $v !== null; - }); - }) - ->listen(BanIPsEvents\IPWasUnbanned::class, 'fof_ban_ips.unbanned', function ($e) { - return array_filter([ - 'ip' => $e->unbannedIP->address, - 'user_id' => $e->unbannedIP->user_id ?: null, - ], function ($v) { - return $v !== null; - }); - }), - ]; - }) - ->whenExtensionEnabled('fof-impersonate', function () { - return [ - (new Audit()) - ->group('fof-impersonate') - ->listen(Impersonated::class, 'user.impersonated', function ($e) { - return [ - 'user_id' => $e->user->id, - 'reason' => $e->switchReason ?: null, - ]; - }), - ]; - }) - ->whenExtensionEnabled('fof-merge-discussions', function () { - return [ - (new Audit()) - ->group('fof-merge-discussions') - ->register('discussion.merged_away', 'discussion.merged_into') - ->using(function () { - // Merge dispatches multiple logs per event, so it uses a raw listener. - resolve('events')->listen(DiscussionWasMerged::class, function (DiscussionWasMerged $event) { - foreach ($event->mergedDiscussions as $discussion) { - AuditLogger::log('discussion.merged_away', [ - 'discussion_id' => $discussion->id, - 'new_discussion_id' => $event->discussion->id, - ]); - } - - AuditLogger::log('discussion.merged_into', [ - 'discussion_id' => $event->discussion->id, - 'original_discussion_ids' => $event->mergedDiscussions->pluck('id')->all(), - 'post_count' => $event->posts->count(), - ]); - }); - }), - ]; - }) - ->whenExtensionEnabled('fof-split', function () { - return [ - (new Audit()) - ->group('fof-split') - ->register('discussion.split_away', 'discussion.split_into') - ->using(function () { - resolve('events')->listen(DiscussionWasSplit::class, function (DiscussionWasSplit $event) { - AuditLogger::log('discussion.split_away', [ - 'discussion_id' => $event->originalDiscussion->id, - 'new_discussion_id' => $event->newDiscussion->id, - 'post_count' => $event->posts->count(), - ]); - AuditLogger::log('discussion.split_into', [ - 'discussion_id' => $event->newDiscussion->id, - 'original_discussion_id' => $event->originalDiscussion->id, - 'post_count' => $event->posts->count(), - ]); - }); - }), - ]; - }) - ->whenExtensionEnabled('fof-user-bio', function () { - return [ - (new Audit()) - ->group('fof-user-bio') - ->listen(BioChanged::class, 'user.bio_changed', function ($e) { - return ['user_id' => $e->user->id]; - }), - ]; - }) - ->whenExtensionEnabled('fof-username-request', function () { - return [ - (new Audit()) - ->group('fof-username-request') - ->using(new Integration\FoFUsernameRequestIntegration()), - ]; - }), -]; diff --git a/extensions/audit/locale/en.yml b/extensions/audit/locale/en.yml index 4c2a111d06..b799e4b821 100644 --- a/extensions/audit/locale/en.yml +++ b/extensions/audit/locale/en.yml @@ -84,19 +84,12 @@ flarum-audit: created: Started discussion {discussion} deleted: Deleted discussion {discussion} hidden: Hid discussion {discussion} - merged_away: Merged {discussion} into {new_discussion} - merged_into: Merged {post_count} posts from {original_discussion_ids_count} discussions into {discussion} renamed: Renamed discussion from {old_title} to {new_title} restored: Restored discussion {discussion} - split_away: Split {post_count} posts from {discussion} into {new_discussion} - split_into: Split {post_count} posts into new discussion {discussion} extension: disabled: Disabled extension {package} enabled: Enabled extension {package} uninstalled: Uninstalled extension {package} - fof_ban_ips: - banned: Banned ip {ip} - unbanned: Unbanned ip {ip} group: created: Created group {name} deleted: Deleted group {name} @@ -116,25 +109,17 @@ flarum-audit: activated_with_email: Activated {username}'s account via confirmation link avatar_changed: Changed {username}'s avatar avatar_removed: Removed {username}'s avatar - bio_changed: Edited {username}'s bio created: Created {username}'s account deleted: Deleted {username} email_changed: Changed {username}'s email from {old_email} to {new_email} email_change_requested: Requested to change {username}'s email to {new_email} groups_changed: Changed {username}'s groups from {old_groups} to {new_groups} - impersonated: Impersonated {username} logged_in: Logged in {username} account logged_in_with_provider: Logged in {username} account using {provider} logged_out: Logged out {username} account - nickname_requested: Requested new nickname {new_nickname} for user {username} - nickname_request_approved: Approved {username}'s nickname change from {old_nickname} to {new_nickname} - nickname_request_rejected: Rejected {username}'s nickname change password_changed: Changed {username}'s password password_change_requested: Requested to change {username}'s password password_reset_attempted: Requested a password reset for {username} password_reset_attempted_unmatched: Requested a password reset for {email} (no matching account) provider_connected: Connected provider {provider} to {username} account username_changed: Changed {username}'s username from {old_username} to {new_username} - username_requested: Requested new username {new_username} for user {username} - username_request_approved: Approved {username}'s username change from {old_username} to {new_username} - username_request_rejected: Rejected {username}'s username change diff --git a/extensions/audit/src/Integration/FoFUsernameRequestIntegration.php b/extensions/audit/src/Integration/FoFUsernameRequestIntegration.php deleted file mode 100644 index 2aad60f79f..0000000000 --- a/extensions/audit/src/Integration/FoFUsernameRequestIntegration.php +++ /dev/null @@ -1,112 +0,0 @@ -make(Dispatcher::class); - - $events->listen('eloquent.updated: '.User::class, [$this, 'userUpdated']); - $events->listen('eloquent.saved: '.UsernameRequest::class, [$this, 'requestSaved']); - } - - public function userUpdated(User $user): void - { - $this->oldNickname = $user->getOriginal('nickname'); - $this->oldUsername = $user->getOriginal('username'); - } - - public function requestSaved(UsernameRequest $request): void - { - switch ($request->status) { - case 'Sent': - if ($request->for_nickname) { - AuditLogger::log('user.nickname_requested', [ - 'user_id' => $request->user_id, - 'new_nickname' => $request->requested_username ?: null, - ]); - } else { - AuditLogger::log('user.username_requested', [ - 'user_id' => $request->user_id, - 'new_username' => $request->requested_username, - ]); - } - break; - case 'Approved': - if ($request->for_nickname) { - AuditLogger::log('user.nickname_request_approved', [ - 'user_id' => $request->user_id, - 'old_nickname' => $this->oldNickname ?: null, - 'new_nickname' => $request->requested_username ?: null, - ]); - } else { - AuditLogger::log('user.username_request_approved', [ - 'user_id' => $request->user_id, - 'old_username' => $this->oldUsername, - 'new_username' => $request->requested_username, - ]); - } - break; - case 'Rejected': - if ($request->for_nickname) { - AuditLogger::log('user.nickname_request_rejected', [ - 'user_id' => $request->user_id, - 'new_nickname' => $request->requested_username ?: null, - 'reason' => $request->reason, - ]); - } else { - AuditLogger::log('user.username_request_rejected', [ - 'user_id' => $request->user_id, - 'new_username' => $request->requested_username, - 'reason' => $request->reason, - ]); - } - break; - } - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFBanIpsTest.php b/extensions/audit/tests/integration/thirdparty/FoFBanIpsTest.php deleted file mode 100644 index ff4f750f5f..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFBanIpsTest.php +++ /dev/null @@ -1,102 +0,0 @@ -extension('fof-ban-ips'); - - $this->prepareDatabase([ - User::class => [ - [ - 'id' => 3, - 'username' => 'user3', - 'email' => 'user3@example.com', - ], - ], - ]); - } - - #[Test] - public function banned() - { - $this->sendSuccessfulRequest('POST', '/api/fof/ban-ips', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'address' => '192.168.2.2', - 'reason' => 'Because', - ], - ], - ], - ], 201); - - $this->assertLogExists('fof_ban_ips.banned', [ - 'ip' => '192.168.2.2', - 'reason' => 'Because', - ]); - } - - #[Test] - public function banned_user() - { - $this->sendSuccessfulRequest('POST', '/api/fof/ban-ips', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'userId' => 3, - 'address' => '192.168.2.3', - 'reason' => 'Because', - ], - ], - ], - ], 201); - - $this->assertLogExists('fof_ban_ips.banned', [ - 'ip' => '192.168.2.3', - 'reason' => 'Because', - 'user_id' => 3, - ]); - } - - // We can't test unbanned without user because the event is not dispatched - // https://github.com/FriendsOfFlarum/ban-ips/issues/4 - - #[Test] - public function unbanned_user() - { - $this->sendSuccessfulRequest('POST', '/api/fof/ban-ips', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'userId' => 3, - 'address' => '192.168.2.4', - 'reason' => 'Because', - ], - ], - ], - ], 201); - - $this->sendSuccessfulRequest('POST', '/api/users/3/unban', []); - - $this->assertLogExists('fof_ban_ips.unbanned', [ - 'ip' => '192.168.2.4', - 'user_id' => 3, - ]); - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFImpersonateTest.php b/extensions/audit/tests/integration/thirdparty/FoFImpersonateTest.php deleted file mode 100644 index 5dedd62db7..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFImpersonateTest.php +++ /dev/null @@ -1,65 +0,0 @@ -extension('fof-impersonate'); - - $this->prepareDatabase([ - User::class => [ - [ - 'id' => 3, - 'username' => 'user3', - 'email' => 'user3@example.com', - ], - ], - ]); - } - - #[Test] - public function impersonate() - { - $adminSession = $this->sendForumCsrfRequest('POST', '/login', [ - 'json' => [ - 'identification' => 'admin', - 'password' => 'password', - ], - ]); - - // We can't use authenticateAs because Impersonate only works with sessions and not access tokens - $response = $this->send($this->request('POST', '/api/impersonate', [ - 'cookiesFrom' => $adminSession, - 'json' => [ - 'data' => [ - 'attributes' => [ - 'userId' => 3, - 'reason' => 'because', // Currently not optional, it would result in undefined index if not included - ], - ], - ], - ])->withAddedHeader('X-CSRF-Token', $adminSession->getHeaderLine('X-CSRF-Token'))); - - $this->assertEquals(200, $response->getStatusCode()); - - $this->assertLogExists('user.impersonated', [ - 'user_id' => 3, - 'reason' => 'because', - ]); - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFMergeTest.php b/extensions/audit/tests/integration/thirdparty/FoFMergeTest.php deleted file mode 100644 index 6cf269e3c5..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFMergeTest.php +++ /dev/null @@ -1,89 +0,0 @@ -extension('fof-merge-discussions'); - - $date = Carbon::parse('2021-01-01T12:00:00+00:00'); - - $this->prepareDatabase([ - Discussion::class => [ - ['id' => 10, 'title' => 'A', 'created_at' => $date, 'last_posted_at' => $date, 'first_post_id' => 1, 'comment_count' => 1], - ['id' => 11, 'title' => 'B', 'created_at' => $date, 'last_posted_at' => $date, 'first_post_id' => 2, 'comment_count' => 1], - ['id' => 12, 'title' => 'C', 'created_at' => $date, 'last_posted_at' => $date, 'first_post_id' => 3, 'comment_count' => 2], - ], - Post::class => [ - ['id' => 1, 'number' => 1, 'discussion_id' => 10, 'created_at' => $date, 'type' => 'comment', 'content' => '

A

'], - ['id' => 2, 'number' => 1, 'discussion_id' => 11, 'created_at' => $date, 'type' => 'comment', 'content' => '

B

'], - ['id' => 3, 'number' => 1, 'discussion_id' => 12, 'created_at' => $date, 'type' => 'comment', 'content' => '

C1

'], - ['id' => 4, 'number' => 2, 'discussion_id' => 12, 'created_at' => $date, 'type' => 'comment', 'content' => '

C2

'], - ], - ]); - } - - #[Test] - public function mergeSingle() - { - $this->sendSuccessfulRequest('POST', '/api/discussions/10/merge', [ - 'json' => [ - 'ids' => '11', - ], - ]); - - $this->assertLogExists('discussion.merged_into', [ - 'discussion_id' => 10, - 'original_discussion_ids' => [11], - 'post_count' => 1, - ]); - - $this->assertLogExists('discussion.merged_away', [ - 'discussion_id' => 11, - 'new_discussion_id' => 10, - ]); - } - - #[Test] - public function mergeMultiple() - { - $this->sendSuccessfulRequest('POST', '/api/discussions/10/merge', [ - 'json' => [ - 'ids' => ['11', '12'], - ], - ]); - - $this->assertLogExists('discussion.merged_into', [ - 'discussion_id' => 10, - 'original_discussion_ids' => [11, 12], - 'post_count' => 3, - ]); - - $this->assertLogExists('discussion.merged_away', [ - 'discussion_id' => 11, - 'new_discussion_id' => 10, - ]); - - $this->assertLogExists('discussion.merged_away', [ - 'discussion_id' => 12, - 'new_discussion_id' => 10, - ], 1, 1); - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFSplitTest.php b/extensions/audit/tests/integration/thirdparty/FoFSplitTest.php deleted file mode 100644 index 7cae7d1c38..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFSplitTest.php +++ /dev/null @@ -1,68 +0,0 @@ -extension('fof-split'); - - $date = Carbon::parse('2021-01-01T12:00:00+00:00'); - - $this->prepareDatabase([ - Discussion::class => [ - ['id' => 10, 'title' => 'A', 'created_at' => $date, 'last_posted_at' => $date, 'first_post_id' => 1, 'comment_count' => 4], - ], - Post::class => [ - ['id' => 1, 'number' => 1, 'discussion_id' => 10, 'created_at' => $date, 'type' => 'comment', 'content' => '

A

'], - ['id' => 2, 'number' => 2, 'discussion_id' => 10, 'created_at' => $date, 'type' => 'comment', 'content' => '

B

', 'user_id' => 1], - ['id' => 3, 'number' => 3, 'discussion_id' => 10, 'created_at' => $date, 'type' => 'comment', 'content' => '

C

'], - ['id' => 4, 'number' => 4, 'discussion_id' => 10, 'created_at' => $date, 'type' => 'comment', 'content' => '

D

'], - ], - ]); - } - - #[Test] - public function split() - { - $response = $this->sendSuccessfulRequest('POST', '/api/split', [ - 'json' => [ - 'title' => 'Split', - 'start_post_id' => 2, - 'end_post_number' => 3, - ], - ]); - - $body = json_decode($response->getBody()->getContents(), true); - $newDiscussionId = Arr::get($body, 'data.id'); - - $this->assertLogExists('discussion.split_away', [ - 'discussion_id' => 10, - 'new_discussion_id' => $newDiscussionId, - 'post_count' => 2, - ]); - - $this->assertLogExists('discussion.split_into', [ - 'discussion_id' => $newDiscussionId, - 'original_discussion_id' => 10, - 'post_count' => 2, - ]); - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFUserBioTest.php b/extensions/audit/tests/integration/thirdparty/FoFUserBioTest.php deleted file mode 100644 index 56e6b2ec5e..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFUserBioTest.php +++ /dev/null @@ -1,52 +0,0 @@ -extension('fof-user-bio'); - - $this->prepareDatabase([ - User::class => [ - [ - 'id' => 3, - 'username' => 'user3', - 'email' => 'user3@example.com', - ], - ], - ]); - } - - #[Test] - public function update() - { - $this->sendSuccessfulRequest('PATCH', '/api/users/3', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'bio' => 'Hello World', - ], - ], - ], - ]); - - $this->assertLogExists('user.bio_changed', [ - 'user_id' => 3, - ]); - } -} diff --git a/extensions/audit/tests/integration/thirdparty/FoFUsernameRequestTest.php b/extensions/audit/tests/integration/thirdparty/FoFUsernameRequestTest.php deleted file mode 100644 index c3a127182f..0000000000 --- a/extensions/audit/tests/integration/thirdparty/FoFUsernameRequestTest.php +++ /dev/null @@ -1,169 +0,0 @@ -extension('fof-username-request', 'flarum-nicknames'); - - $this->prepareDatabase([ - User::class => [ - [ - 'id' => 3, - 'username' => 'user3', - 'email' => 'user3@example.com', - ], - ], - 'username_requests' => [ - ['id' => 1, 'user_id' => 3, 'requested_username' => 'user33', 'status' => 'Sent'], - ['id' => 2, 'user_id' => 3, 'requested_username' => 'user33', 'status' => 'Sent', 'for_nickname' => true], - ], - ]); - } - - #[Test] - public function createUsername() - { - $this->sendSuccessfulRequest('POST', '/api/username-requests', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'username' => 'admin2', - ], - ], - 'meta' => [ - 'password' => 'password', - ], - ], - ], 201); - - $this->assertLogExists('user.username_requested', [ - 'user_id' => 1, - 'new_username' => 'admin2', - ]); - } - - #[Test] - public function createNickname() - { - $this->sendSuccessfulRequest('POST', '/api/username-requests', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'username' => 'admin2', - 'forNickname' => true, - ], - ], - 'meta' => [ - 'password' => 'password', - ], - ], - ], 201); - - $this->assertLogExists('user.nickname_requested', [ - 'user_id' => 1, - 'new_nickname' => 'admin2', - ]); - } - - #[Test] - public function approveUsername() - { - $this->sendSuccessfulRequest('PATCH', '/api/username-requests/1', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'action' => 'Approved', - ], - ], - ], - ]); - - $this->assertLogExists('user.username_request_approved', [ - 'user_id' => 3, - 'old_username' => 'user3', - 'new_username' => 'user33', - ]); - - $this->assertLogDoesntExist('user.username_changed'); - } - - #[Test] - public function approveNickname() - { - $this->sendSuccessfulRequest('PATCH', '/api/username-requests/2', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'action' => 'Approved', - ], - ], - ], - ]); - - $this->assertLogExists('user.nickname_request_approved', [ - 'user_id' => 3, - 'old_nickname' => null, - 'new_nickname' => 'user33', - ]); - - $this->assertLogDoesntExist('user.nickname_changed'); - } - - #[Test] - public function rejectUsername() - { - $this->sendSuccessfulRequest('PATCH', '/api/username-requests/1', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'action' => 'Rejected', - 'reason' => 'because', - ], - ], - ], - ]); - - $this->assertLogExists('user.username_request_rejected', [ - 'user_id' => 3, - 'new_username' => 'user33', - 'reason' => 'because', - ]); - } - - #[Test] - public function rejectNickname() - { - $this->sendSuccessfulRequest('PATCH', '/api/username-requests/2', [ - 'json' => [ - 'data' => [ - 'attributes' => [ - 'action' => 'Rejected', - 'reason' => 'because', - ], - ], - ], - ]); - - $this->assertLogExists('user.nickname_request_rejected', [ - 'user_id' => 3, - 'new_nickname' => 'user33', - 'reason' => 'because', - ]); - } -} diff --git a/extensions/audit/tests/phpunit.integration.xml b/extensions/audit/tests/phpunit.integration.xml index fc308069e8..849ceb516e 100644 --- a/extensions/audit/tests/phpunit.integration.xml +++ b/extensions/audit/tests/phpunit.integration.xml @@ -19,8 +19,6 @@ ./integration ./integration/tmp - - ./integration/thirdparty diff --git a/extensions/audit/tests/unit/IntegrationActionRegistrationTest.php b/extensions/audit/tests/unit/IntegrationActionRegistrationTest.php index ec266bc688..bae48246c5 100644 --- a/extensions/audit/tests/unit/IntegrationActionRegistrationTest.php +++ b/extensions/audit/tests/unit/IntegrationActionRegistrationTest.php @@ -12,7 +12,6 @@ use Flarum\Audit\Extend\Audit; use Flarum\Audit\Integration\CoreSettingIntegration; use Flarum\Audit\Integration\CoreUserIntegration; -use Flarum\Audit\Integration\FoFUsernameRequestIntegration; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -86,13 +85,12 @@ public function register_and_listen_also_contribute_actions() public static function integrations(): array { - // First-party extension integrations (flags, nicknames, tags) now live in their own - // extensions and are covered by those extensions' AuditTest suites. Only the core and - // bundled-thirdparty integrations that still ship with flarum/audit are exercised here. + // Third-party extension integrations now live in their own extensions and are covered + // by those extensions' AuditTest suites. Only the core integrations that ship with + // flarum/audit are exercised here. return [ 'core user' => [CoreUserIntegration::class], 'core setting' => [CoreSettingIntegration::class], - 'fof username request' => [FoFUsernameRequestIntegration::class], ]; } } diff --git a/phpstan.neon b/phpstan.neon index fdc8567a0d..c59e59e6df 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -43,7 +43,4 @@ parameters: - extensions/gdpr/src excludePaths: - *.blade.php - # Audit integrations for third-party extensions reference classes outside the monorepo. - - extensions/audit/extend.thirdparty.php - - extensions/audit/src/Integration/FoFUsernameRequestIntegration.php databaseMigrationsPath: ['framework/core/migrations']