diff --git a/migrations/Version20260225095315.php b/migrations/Version20260225095315.php
new file mode 100644
index 000000000..a2ed69623
--- /dev/null
+++ b/migrations/Version20260225095315.php
@@ -0,0 +1,33 @@
+addSql('ALTER TABLE magazine_log ADD author_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE magazine_log ADD title VARCHAR(255) DEFAULT NULL');
+ $this->addSql('ALTER TABLE magazine_log ADD CONSTRAINT FK_87D3D4C5F675F31B FOREIGN KEY (author_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('CREATE INDEX IDX_87D3D4C5F675F31B ON magazine_log (author_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE magazine_log DROP CONSTRAINT FK_87D3D4C5F675F31B');
+ $this->addSql('DROP INDEX IDX_87D3D4C5F675F31B');
+ $this->addSql('ALTER TABLE magazine_log DROP author_id');
+ $this->addSql('ALTER TABLE magazine_log DROP title');
+ $this->addSql('ALTER TABLE magazine_log DROP short_body');
+ }
+}
diff --git a/src/Controller/ModlogController.php b/src/Controller/ModlogController.php
index bcfdfe112..e96f191d3 100644
--- a/src/Controller/ModlogController.php
+++ b/src/Controller/ModlogController.php
@@ -23,7 +23,7 @@ public function instance(Request $request): Response
{
$dto = new ModlogFilterDto();
$dto->magazine = null;
- $form = $this->createForm(ModlogFilterType::class, $dto, ['method' => 'GET']);
+ $form = $this->createForm(ModlogFilterType::class, $dto, ['method' => 'GET', 'csrf_protection' => false]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var ModlogFilterDto $dto */
diff --git a/src/DTO/MagazineLogResponseDto.php b/src/DTO/MagazineLogResponseDto.php
index 1b84f7bf6..e8fb923be 100644
--- a/src/DTO/MagazineLogResponseDto.php
+++ b/src/DTO/MagazineLogResponseDto.php
@@ -20,15 +20,19 @@
#[OA\Schema()]
class MagazineLogResponseDto implements \JsonSerializable
{
- public const LOG_TYPES = [
+ public const array LOG_TYPES = [
'log_entry_deleted',
'log_entry_restored',
+ 'log_entry_purged',
'log_entry_comment_deleted',
'log_entry_comment_restored',
+ 'log_entry_comment_purged',
'log_post_deleted',
'log_post_restored',
+ 'log_post_purged',
'log_post_comment_deleted',
'log_post_comment_restored',
+ 'log_post_comment_purged',
'log_ban',
'log_unban',
'log_entry_pinned',
@@ -48,11 +52,17 @@ class MagazineLogResponseDto implements \JsonSerializable
* - PostCommentResponseDto when type is 'log_post_comment_deleted' or 'log_post_comment_restored'
* - MagazineBanResponseDto when type is 'log_ban' or 'log_unban'
* - UserSmallResponseDto when type is 'log_moderator_add' or 'log_moderator_remove'
+ * - string when type is 'log_entry_purged', 'log_entry_comment_purged', 'log_post_purged' or 'log_post_comment_purged'
*/
#[OA\Property('subject')]
// If this property is named 'subject' the api doc generator will not pick it up.
// It is still serialized as 'subject', see the jsonSerialize method.
- public EntryResponseDto|EntryCommentResponseDto|PostResponseDto|PostCommentResponseDto|MagazineBanResponseDto|UserSmallResponseDto|null $subject2 = null;
+ public EntryResponseDto|EntryCommentResponseDto|PostResponseDto|PostCommentResponseDto|MagazineBanResponseDto|UserSmallResponseDto|string|null $subject2 = null;
+
+ /**
+ * Only set if the subject is a string, otherwise the author information is contained within the subject.
+ */
+ public ?UserSmallResponseDto $subjectAuthor = null;
public static function create(
MagazineSmallResponseDto $magazine,
@@ -139,7 +149,8 @@ public function jsonSerialize(): mixed
'createdAt' => $this->createdAt->format(\DateTimeInterface::ATOM),
'magazine' => $this->magazine,
'moderator' => $this->moderator,
- 'subject' => $this->subject2?->jsonSerialize(),
+ 'subject' => \is_string($this->subject2) ? $this->subject2 : $this->subject2?->jsonSerialize(),
+ 'subjectAuthor' => $this->subjectAuthor?->jsonSerialize(),
];
}
}
diff --git a/src/Entity/MagazineLog.php b/src/Entity/MagazineLog.php
index a6d88c1b9..2f9a5b285 100644
--- a/src/Entity/MagazineLog.php
+++ b/src/Entity/MagazineLog.php
@@ -27,17 +27,21 @@ abstract class MagazineLog
CreatedAtTrait::__construct as createdAtTraitConstruct;
}
- public const DISCRIMINATOR_MAP = [
+ public const array DISCRIMINATOR_MAP = [
'entry_deleted' => MagazineLogEntryDeleted::class,
'entry_restored' => MagazineLogEntryRestored::class,
+ 'entry_purged' => MagazineLogEntryPurged::class,
'entry_comment_deleted' => MagazineLogEntryCommentDeleted::class,
'entry_comment_restored' => MagazineLogEntryCommentRestored::class,
+ 'entry_comment_purged' => MagazineLogEntryCommentPurged::class,
'entry_pinned' => MagazineLogEntryPinned::class,
'entry_unpinned' => MagazineLogEntryUnpinned::class,
'post_deleted' => MagazineLogPostDeleted::class,
'post_restored' => MagazineLogPostRestored::class,
+ 'post_purged' => MagazineLogPostPurged::class,
'post_comment_deleted' => MagazineLogPostCommentDeleted::class,
'post_comment_restored' => MagazineLogPostCommentRestored::class,
+ 'post_comment_purged' => MagazineLogPostCommentPurged::class,
'ban' => MagazineLogBan::class,
'moderator_add' => MagazineLogModeratorAdd::class,
'moderator_remove' => MagazineLogModeratorRemove::class,
@@ -47,17 +51,21 @@ abstract class MagazineLog
'post_unlocked' => MagazineLogPostUnlocked::class,
];
- public const CHOICES = [
+ public const array CHOICES = [
'entry_deleted',
'entry_restored',
+ 'entry_purged',
'entry_comment_deleted',
'entry_comment_restored',
+ 'entry_comment_purged',
'entry_pinned',
'entry_unpinned',
'post_deleted',
'post_restored',
+ 'post_purged',
'post_comment_deleted',
'post_comment_restored',
+ 'post_comment_purged',
'ban',
'moderator_add',
'moderator_remove',
diff --git a/src/Entity/MagazineLogEntryCommentPurged.php b/src/Entity/MagazineLogEntryCommentPurged.php
new file mode 100644
index 000000000..bb6581d88
--- /dev/null
+++ b/src/Entity/MagazineLogEntryCommentPurged.php
@@ -0,0 +1,44 @@
+title = $title;
+ $this->author = $author;
+ }
+
+ public function getSubject(): ?ContentInterface
+ {
+ return null;
+ }
+
+ public function clearSubject(): MagazineLog
+ {
+ return $this;
+ }
+
+ public function getType(): string
+ {
+ return 'log_entry_comment_purged';
+ }
+}
diff --git a/src/Entity/MagazineLogEntryPurged.php b/src/Entity/MagazineLogEntryPurged.php
new file mode 100644
index 000000000..1d9eef826
--- /dev/null
+++ b/src/Entity/MagazineLogEntryPurged.php
@@ -0,0 +1,44 @@
+title = $title;
+ $this->author = $author;
+ }
+
+ public function getSubject(): ?ContentInterface
+ {
+ return null;
+ }
+
+ public function clearSubject(): MagazineLog
+ {
+ return $this;
+ }
+
+ public function getType(): string
+ {
+ return 'log_entry_purged';
+ }
+}
diff --git a/src/Entity/MagazineLogPostCommentPurged.php b/src/Entity/MagazineLogPostCommentPurged.php
new file mode 100644
index 000000000..bcad362dd
--- /dev/null
+++ b/src/Entity/MagazineLogPostCommentPurged.php
@@ -0,0 +1,44 @@
+title = $title;
+ $this->author = $author;
+ }
+
+ public function getSubject(): ?ContentInterface
+ {
+ return null;
+ }
+
+ public function clearSubject(): MagazineLog
+ {
+ return $this;
+ }
+
+ public function getType(): string
+ {
+ return 'log_post_comment_purged';
+ }
+}
diff --git a/src/Entity/MagazineLogPostPurged.php b/src/Entity/MagazineLogPostPurged.php
new file mode 100644
index 000000000..6b1dfd555
--- /dev/null
+++ b/src/Entity/MagazineLogPostPurged.php
@@ -0,0 +1,44 @@
+title = $title;
+ $this->author = $author;
+ }
+
+ public function getSubject(): ?ContentInterface
+ {
+ return null;
+ }
+
+ public function clearSubject(): MagazineLog
+ {
+ return $this;
+ }
+
+ public function getType(): string
+ {
+ return 'log_post_purged';
+ }
+}
diff --git a/src/EventSubscriber/Magazine/MagazineLogSubscriber.php b/src/EventSubscriber/Magazine/MagazineLogSubscriber.php
index 44fc489f5..5609728f1 100644
--- a/src/EventSubscriber/Magazine/MagazineLogSubscriber.php
+++ b/src/EventSubscriber/Magazine/MagazineLogSubscriber.php
@@ -6,29 +6,40 @@
use App\Entity\MagazineLogBan;
use App\Entity\MagazineLogEntryCommentDeleted;
+use App\Entity\MagazineLogEntryCommentPurged;
use App\Entity\MagazineLogEntryCommentRestored;
use App\Entity\MagazineLogEntryDeleted;
+use App\Entity\MagazineLogEntryPurged;
use App\Entity\MagazineLogEntryRestored;
use App\Entity\MagazineLogPostCommentDeleted;
+use App\Entity\MagazineLogPostCommentPurged;
use App\Entity\MagazineLogPostCommentRestored;
use App\Entity\MagazineLogPostDeleted;
+use App\Entity\MagazineLogPostPurged;
use App\Entity\MagazineLogPostRestored;
+use App\Event\Entry\EntryBeforePurgeEvent;
use App\Event\Entry\EntryDeletedEvent;
use App\Event\Entry\EntryRestoredEvent;
+use App\Event\EntryComment\EntryCommentBeforePurgeEvent;
use App\Event\EntryComment\EntryCommentDeletedEvent;
use App\Event\EntryComment\EntryCommentRestoredEvent;
use App\Event\Magazine\MagazineBanEvent;
+use App\Event\Post\PostBeforePurgeEvent;
use App\Event\Post\PostDeletedEvent;
use App\Event\Post\PostRestoredEvent;
+use App\Event\PostComment\PostCommentBeforePurgeEvent;
use App\Event\PostComment\PostCommentDeletedEvent;
use App\Event\PostComment\PostCommentRestoredEvent;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MagazineLogSubscriber implements EventSubscriberInterface
{
- public function __construct(private readonly EntityManagerInterface $entityManager)
- {
+ public function __construct(
+ private readonly EntityManagerInterface $entityManager,
+ private readonly LoggerInterface $logger,
+ ) {
}
public static function getSubscribedEvents(): array
@@ -36,12 +47,16 @@ public static function getSubscribedEvents(): array
return [
EntryDeletedEvent::class => 'onEntryDeleted',
EntryRestoredEvent::class => 'onEntryRestored',
+ EntryBeforePurgeEvent::class => 'onEntryPurged',
EntryCommentDeletedEvent::class => 'onEntryCommentDeleted',
EntryCommentRestoredEvent::class => 'onEntryCommentRestored',
+ EntryCommentBeforePurgeEvent::class => 'onEntryCommentPurged',
PostDeletedEvent::class => 'onPostDeleted',
PostRestoredEvent::class => 'onPostRestored',
+ PostBeforePurgeEvent::class => 'onPostPurged',
PostCommentDeletedEvent::class => 'onPostCommentDeleted',
PostCommentRestoredEvent::class => 'onPostCommentRestored',
+ PostCommentBeforePurgeEvent::class => 'onPostCommentPurged',
MagazineBanEvent::class => 'onBan',
];
}
@@ -78,6 +93,19 @@ public function onEntryRestored(EntryRestoredEvent $event): void
$this->entityManager->flush();
}
+ public function onEntryPurged(EntryBeforePurgeEvent $event): void
+ {
+ if (!$event->user || $event->entry->isAuthor($event->user)) {
+ return;
+ }
+
+ $log = new MagazineLogEntryPurged($event->entry->magazine, $event->user, $event->entry->title, $event->entry->user);
+ $this->logger->info('Entry "{t}" from {u} was purged by mod {u2}', ['t' => $log->title, 'u' => $log->author->username, 'u2' => $log->user->username]);
+
+ $this->entityManager->persist($log);
+ $this->entityManager->flush();
+ }
+
public function onEntryCommentDeleted(EntryCommentDeletedEvent $event): void
{
if (!$event->comment->isTrashed()) {
@@ -110,6 +138,19 @@ public function onEntryCommentRestored(EntryCommentRestoredEvent $event): void
$this->entityManager->flush();
}
+ public function onEntryCommentPurged(EntryCommentBeforePurgeEvent $event): void
+ {
+ if (!$event->user || $event->comment->isAuthor($event->user)) {
+ return;
+ }
+
+ $log = new MagazineLogEntryCommentPurged($event->comment->magazine, $event->user, $event->comment->getShortTitle(), $event->comment->user);
+ $this->logger->info('Entry comment "{t}" from {u} was purged by mod {u2}', ['t' => $log->title, 'u' => $log->author->username, 'u2' => $log->user->username]);
+
+ $this->entityManager->persist($log);
+ $this->entityManager->flush();
+ }
+
public function onPostDeleted(PostDeletedEvent $event): void
{
if (!$event->post->isTrashed()) {
@@ -142,6 +183,19 @@ public function onPostRestored(PostRestoredEvent $event): void
$this->entityManager->flush();
}
+ public function onPostPurged(PostBeforePurgeEvent $event): void
+ {
+ if (!$event->user || $event->post->isAuthor($event->user)) {
+ return;
+ }
+
+ $log = new MagazineLogPostPurged($event->post->magazine, $event->user, $event->post->getShortTitle(), $event->post->user);
+ $this->logger->info('Post "{t}" from {u} was purged by mod {u2}', ['t' => $log->title, 'u' => $log->author->username, 'u2' => $log->user->username]);
+
+ $this->entityManager->persist($log);
+ $this->entityManager->flush();
+ }
+
public function onPostCommentDeleted(PostCommentDeletedEvent $event): void
{
if (!$event->comment->isTrashed()) {
@@ -174,6 +228,19 @@ public function onPostCommentRestored(PostCommentRestoredEvent $event): void
$this->entityManager->flush();
}
+ public function onPostCommentPurged(PostCommentBeforePurgeEvent $event): void
+ {
+ if (!$event->user || $event->comment->isAuthor($event->user)) {
+ return;
+ }
+
+ $log = new MagazineLogPostCommentPurged($event->comment->magazine, $event->user, $event->comment->getShortTitle(), $event->comment->user);
+ $this->logger->info('Post comment "{t}" from {u} was purged by mod {u2}', ['t' => $log->title, 'u' => $log->author->username, 'u2' => $log->user->username]);
+
+ $this->entityManager->persist($log);
+ $this->entityManager->flush();
+ }
+
public function onBan(MagazineBanEvent $event): void
{
$log = new MagazineLogBan($event->ban);
diff --git a/src/Factory/MagazineFactory.php b/src/Factory/MagazineFactory.php
index dc4e9b3f8..5c2558757 100644
--- a/src/Factory/MagazineFactory.php
+++ b/src/Factory/MagazineFactory.php
@@ -16,8 +16,12 @@
use App\Entity\MagazineBan;
use App\Entity\MagazineLog;
use App\Entity\MagazineLogBan;
+use App\Entity\MagazineLogEntryCommentPurged;
+use App\Entity\MagazineLogEntryPurged;
use App\Entity\MagazineLogModeratorAdd;
use App\Entity\MagazineLogModeratorRemove;
+use App\Entity\MagazineLogPostCommentPurged;
+use App\Entity\MagazineLogPostPurged;
use App\Entity\Moderator;
use App\Entity\User;
use App\Repository\InstanceRepository;
@@ -136,6 +140,14 @@ public function createLogDto(MagazineLog $log): MagazineLogResponseDto
}
return MagazineLogResponseDto::createBanUnban($magazine, $moderator, $createdAt, $type, $banSubject);
+ } elseif ($log instanceof MagazineLogEntryPurged || $log instanceof MagazineLogEntryCommentPurged || $log instanceof MagazineLogPostPurged || $log instanceof MagazineLogPostCommentPurged) {
+ $moderator = $this->userFactory->createSmallDto($log->user);
+ $author = $this->userFactory->createSmallDto($log->author);
+ $dto = MagazineLogResponseDto::create($magazine, $moderator, $createdAt, $type);
+ $dto->subject2 = $log->title;
+ $dto->subjectAuthor = $author;
+
+ return $dto;
} else {
$moderator = $this->userFactory->createSmallDto($log->user);
diff --git a/src/Form/ModlogFilterType.php b/src/Form/ModlogFilterType.php
index 9c35121d8..f8df86ffa 100644
--- a/src/Form/ModlogFilterType.php
+++ b/src/Form/ModlogFilterType.php
@@ -26,14 +26,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'choices' => [
$this->translator->trans('modlog_type_entry_deleted') => 'entry_deleted',
$this->translator->trans('modlog_type_entry_restored') => 'entry_restored',
+ $this->translator->trans('modlog_type_entry_purged') => 'entry_purged',
$this->translator->trans('modlog_type_entry_comment_deleted') => 'entry_comment_deleted',
$this->translator->trans('modlog_type_entry_comment_restored') => 'entry_comment_restored',
+ $this->translator->trans('modlog_type_entry_comment_purged') => 'entry_comment_purged',
$this->translator->trans('modlog_type_entry_pinned') => 'entry_pinned',
$this->translator->trans('modlog_type_entry_unpinned') => 'entry_unpinned',
$this->translator->trans('modlog_type_post_deleted') => 'post_deleted',
$this->translator->trans('modlog_type_post_restored') => 'post_restored',
+ $this->translator->trans('modlog_type_post_purged') => 'post_purged',
$this->translator->trans('modlog_type_post_comment_deleted') => 'post_comment_deleted',
$this->translator->trans('modlog_type_post_comment_restored') => 'post_comment_restored',
+ $this->translator->trans('modlog_type_post_comment_purged') => 'post_comment_purged',
$this->translator->trans('modlog_type_ban') => 'ban',
$this->translator->trans('modlog_type_moderator_add') => 'moderator_add',
$this->translator->trans('modlog_type_moderator_remove') => 'moderator_remove',
diff --git a/templates/modlog/_blocks.html.twig b/templates/modlog/_blocks.html.twig
index 154742645..56ea168e2 100644
--- a/templates/modlog/_blocks.html.twig
+++ b/templates/modlog/_blocks.html.twig
@@ -8,6 +8,11 @@
{{ log.entry.shortTitle(300) }}
{% endblock %}
+{% block log_entry_purged %}
+ {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'purged_thread_by'|trans|lower }} {{ component('user_inline', {user: log.author, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
+ {{ 'former_title'|trans }}: {{ log.title }}
+{% endblock %}
+
{% block log_entry_comment_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
@@ -18,6 +23,11 @@
{{ log.comment.shortTitle(300) }}
{% endblock %}
+{% block log_entry_comment_purged %}
+ {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'purged_comment_by'|trans|lower }} {{ component('user_inline', {user: log.author, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
+ {{ 'former_title'|trans }}: {{ log.title }}
+{% endblock %}
+
{% block log_post_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_post_by'|trans|lower }} {{ component('user_inline', {user: log.post.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.post.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.post.shortTitle(300) }}
@@ -28,6 +38,11 @@
{{ log.post.shortTitle(300) }}
{% endblock %}
+{% block log_post_purged %}
+ {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'purged_post_by'|trans|lower }} {{ component('user_inline', {user: log.author, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
+ {{ 'former_title'|trans }}: {{ log.title }}
+{% endblock %}
+
{% block log_post_comment_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
@@ -38,6 +53,11 @@
{{ log.comment.shortTitle(300) }}
{% endblock %}
+{% block log_post_comment_purged %}
+ {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'purged_comment_by'|trans|lower }} {{ component('user_inline', {user: log.author, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
+ {{ 'former_title'|trans }}: {{ log.title }}
+{% endblock %}
+
{% block log_ban %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% if log.meta is same as 'ban' %}
diff --git a/tests/Functional/Controller/Api/Magazine/MagazinePurgeContentModLogApiTest.php b/tests/Functional/Controller/Api/Magazine/MagazinePurgeContentModLogApiTest.php
new file mode 100644
index 000000000..d3dee5085
--- /dev/null
+++ b/tests/Functional/Controller/Api/Magazine/MagazinePurgeContentModLogApiTest.php
@@ -0,0 +1,95 @@
+getEntryByTitle('entry');
+ $admin = $this->getUserByUsername('admin', isAdmin: true);
+ $this->entryManager->purge($admin, $entry);
+
+ $this->client->request('GET', "/api/magazine/{$entry->magazine->getId()}/log");
+
+ self::assertResponseIsSuccessful();
+ $jsonData = self::getJsonResponse($this->client);
+
+ self::assertIsArray($jsonData);
+ self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
+ self::assertIsArray($jsonData['items']);
+ self::assertCount(1, $jsonData['items']);
+ $this->validateModlog($jsonData, $entry->magazine, $admin);
+ self::assertEquals($entry->title, $jsonData['items'][0]['subject']);
+ }
+
+ public function testPurgeEntryCommentModLog(): void
+ {
+ $entry = $this->getEntryByTitle('entry');
+ $entryComment = $this->createEntryComment('entryComment', entry: $entry);
+ $admin = $this->getUserByUsername('admin', isAdmin: true);
+
+ // otherwise we get persisting problems
+ $this->entityManager->remove($this->activityRepository->findFirstActivitiesByTypeAndObject('Create', $entryComment));
+ $this->entryCommentManager->purge($admin, $entryComment);
+
+ $this->client->request('GET', "/api/magazine/{$entry->magazine->getId()}/log");
+
+ self::assertResponseIsSuccessful();
+ $jsonData = self::getJsonResponse($this->client);
+
+ self::assertIsArray($jsonData);
+ self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
+ self::assertIsArray($jsonData['items']);
+ self::assertCount(1, $jsonData['items']);
+ $this->validateModlog($jsonData, $entry->magazine, $admin);
+ self::assertEquals($entryComment->getShortTitle(), $jsonData['items'][0]['subject']);
+ }
+
+ public function testPurgePostModLog(): void
+ {
+ $post = $this->createPost('post');
+ $admin = $this->getUserByUsername('admin', isAdmin: true);
+ $this->postManager->purge($admin, $post);
+
+ $this->client->request('GET', "/api/magazine/{$post->magazine->getId()}/log");
+
+ self::assertResponseIsSuccessful();
+ $jsonData = self::getJsonResponse($this->client);
+
+ self::assertIsArray($jsonData);
+ self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
+ self::assertIsArray($jsonData['items']);
+ self::assertCount(1, $jsonData['items']);
+ $this->validateModlog($jsonData, $post->magazine, $admin);
+ self::assertEquals($post->getShortTitle(), $jsonData['items'][0]['subject']);
+ }
+
+ public function testPurgePostCommentModLog(): void
+ {
+ $post = $this->createPost('post');
+ $postComment = $this->createPostComment('postComment', post: $post);
+ $admin = $this->getUserByUsername('admin', isAdmin: true);
+
+ // otherwise we get persisting problems
+ $this->entityManager->remove($this->activityRepository->findFirstActivitiesByTypeAndObject('Create', $postComment));
+
+ $this->postCommentManager->purge($admin, $postComment);
+
+ $this->client->request('GET', "/api/magazine/{$post->magazine->getId()}/log");
+
+ self::assertResponseIsSuccessful();
+ $jsonData = self::getJsonResponse($this->client);
+
+ self::assertIsArray($jsonData);
+ self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
+ self::assertIsArray($jsonData['items']);
+ self::assertCount(1, $jsonData['items']);
+ $this->validateModlog($jsonData, $post->magazine, $admin);
+ self::assertEquals($postComment->getShortTitle(), $jsonData['items'][0]['subject']);
+ }
+}
diff --git a/tests/ValidationTrait.php b/tests/ValidationTrait.php
index 0079b66d8..e682e0d2d 100644
--- a/tests/ValidationTrait.php
+++ b/tests/ValidationTrait.php
@@ -42,6 +42,13 @@ public function validateModlog(array $jsonData, Magazine $magazine, User $modera
case 'log_unban':
self::assertArrayKeysMatch(WebTestCase::BAN_RESPONSE_KEYS, $item['subject']);
break;
+ case 'log_entry_purged':
+ case 'log_entry_comment_purged':
+ case 'log_post_purged':
+ case 'log_post_comment_purged':
+ self::assertTrue(\is_string($item['subject']));
+ self::assertNotNull($item['subjectAuthor']);
+ break;
default:
self::assertTrue(false, 'This should not be reached');
break;
diff --git a/tests/WebTestCase.php b/tests/WebTestCase.php
index 5e0747887..9b8fbc4c0 100644
--- a/tests/WebTestCase.php
+++ b/tests/WebTestCase.php
@@ -93,7 +93,7 @@ abstract class WebTestCase extends BaseWebTestCase
protected const POST_RESPONSE_KEYS = ['postId', 'user', 'magazine', 'image', 'body', 'lang', 'isAdult', 'isPinned', 'isLocked', 'comments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'tags', 'mentions', 'createdAt', 'editedAt', 'lastActive', 'slug', 'canAuthUserModerate', 'notificationStatus', 'bookmarks', 'isAuthorModeratorInMagazine'];
protected const POST_COMMENT_RESPONSE_KEYS = ['commentId', 'user', 'magazine', 'postId', 'parentId', 'rootId', 'image', 'body', 'lang', 'isAdult', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'mentions', 'tags', 'createdAt', 'editedAt', 'lastActive', 'childCount', 'children', 'canAuthUserModerate', 'bookmarks', 'isAuthorModeratorInMagazine'];
protected const BAN_RESPONSE_KEYS = ['banId', 'reason', 'expired', 'expiredAt', 'bannedUser', 'bannedBy', 'magazine'];
- protected const LOG_ENTRY_KEYS = ['type', 'createdAt', 'magazine', 'moderator', 'subject'];
+ protected const LOG_ENTRY_KEYS = ['type', 'createdAt', 'magazine', 'moderator', 'subject', 'subjectAuthor'];
protected const MAGAZINE_RESPONSE_KEYS = ['magazineId', 'owner', 'icon', 'banner', 'name', 'title', 'description', 'rules', 'subscriptionsCount', 'entryCount', 'entryCommentCount', 'postCount', 'postCommentCount', 'isAdult', 'isUserSubscribed', 'isBlockedByUser', 'tags', 'badges', 'moderators', 'apId', 'apProfileId', 'serverSoftware', 'serverSoftwareVersion', 'isPostingRestrictedToMods', 'localSubscribers', 'notificationStatus', 'discoverable', 'indexable'];
protected const MAGAZINE_SMALL_RESPONSE_KEYS = ['magazineId', 'name', 'icon', 'banner', 'isUserSubscribed', 'isBlockedByUser', 'apId', 'apProfileId', 'discoverable', 'indexable'];
protected const DOMAIN_RESPONSE_KEYS = ['domainId', 'name', 'entryCount', 'subscriptionsCount', 'isUserSubscribed', 'isBlockedByUser'];
diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml
index d0c3c69ba..1f9b1f6ae 100644
--- a/translations/messages.en.yaml
+++ b/translations/messages.en.yaml
@@ -1143,3 +1143,11 @@ magazine_name_as_tag_help: The tags of a magazine are used to match microblog po
containing "#fediverse" will be put in this magazine.
magazine_rules_deprecated: the rules field is deprecated and will be removed in the future.
Please put your rules in the description box.
+purged_thread_by: purged thread by
+purged_comment_by: purged comment by
+purged_post_by: purged microblog by
+former_title: Former title
+modlog_type_entry_purged: Thread purged
+modlog_type_entry_comment_purged: Thread comment purged
+modlog_type_post_purged: Microblog purged
+modlog_type_post_comment_purged: Microblog reply purged