diff --git a/.env.test b/.env.test
index 6a0870af74..69d88dd91f 100644
--- a/.env.test
+++ b/.env.test
@@ -17,6 +17,7 @@ MAILER_DSN=null://default
KBIN_JS_ENABLED=false
KBIN_DEFAULT_LANG=en
KBIN_DOMAIN=kbin.test
+KBIN_STORAGE_URL=https://kbin.test/media
ELASTICSEARCH_ENABLED=false
KBIN_API_ITEMS_PER_PAGE=2
KBIN_FEDERATION_ENABLED=true
diff --git a/migrations/Version20260113103210.php b/migrations/Version20260113103210.php
new file mode 100644
index 0000000000..6181d9a1fb
--- /dev/null
+++ b/migrations/Version20260113103210.php
@@ -0,0 +1,32 @@
+addSql('ALTER TABLE magazine ADD ap_indexable BOOLEAN DEFAULT NULL');
+ $this->addSql('ALTER TABLE "user" ADD ap_indexable BOOLEAN DEFAULT NULL');
+ // The column should be nullable so that we know whether other software simply does not set this value,
+ // but for local users and magazines we should only have true and false as options
+ $this->addSql('UPDATE "user" SET ap_indexable = true WHERE ap_id IS NULL');
+ $this->addSql('UPDATE magazine SET ap_indexable = true WHERE ap_id IS NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE "user" DROP ap_indexable');
+ $this->addSql('ALTER TABLE magazine DROP ap_indexable');
+ }
+}
diff --git a/src/DTO/MagazineDto.php b/src/DTO/MagazineDto.php
index b6a21c1199..8dcf20e561 100644
--- a/src/DTO/MagazineDto.php
+++ b/src/DTO/MagazineDto.php
@@ -40,6 +40,7 @@ class MagazineDto
public int $postCommentCount = 0;
public bool $isAdult = false;
public bool $isPostingRestrictedToMods = false;
+ public ?bool $indexable = null;
public ?bool $isUserSubscribed = null;
public ?bool $isBlockedByUser = null;
public ?int $localSubscribers = null;
diff --git a/src/DTO/MagazineRequestDto.php b/src/DTO/MagazineRequestDto.php
index 90b1a8886f..cb33dd14dd 100644
--- a/src/DTO/MagazineRequestDto.php
+++ b/src/DTO/MagazineRequestDto.php
@@ -16,6 +16,7 @@ class MagazineRequestDto
public ?bool $isAdult = null;
public ?bool $isPostingRestrictedToMods = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
public function mergeIntoDto(MagazineDto $dto): MagazineDto
{
@@ -26,6 +27,7 @@ public function mergeIntoDto(MagazineDto $dto): MagazineDto
$dto->isAdult = null !== $this->isAdult ? $this->isAdult : $dto->isAdult;
$dto->isPostingRestrictedToMods = $this->isPostingRestrictedToMods ?? false;
$dto->discoverable = $this->discoverable ?? $dto->discoverable ?? true;
+ $dto->indexable = $this->indexable ?? $dto->indexable ?? true;
return $dto;
}
diff --git a/src/DTO/MagazineResponseDto.php b/src/DTO/MagazineResponseDto.php
index 76075b3792..7902e93a24 100644
--- a/src/DTO/MagazineResponseDto.php
+++ b/src/DTO/MagazineResponseDto.php
@@ -41,6 +41,7 @@ class MagazineResponseDto implements \JsonSerializable
public ?int $localSubscribers = null;
public ?ENotificationStatus $notificationStatus = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
public static function create(
?ModeratorResponseDto $owner = null,
@@ -69,6 +70,7 @@ public static function create(
bool $isPostingRestrictedToMods = false,
?int $localSubscribers = null,
?bool $discoverable = null,
+ ?bool $indexable = null,
): self {
$dto = new MagazineResponseDto();
$dto->owner = $owner;
@@ -97,6 +99,7 @@ public static function create(
$dto->isPostingRestrictedToMods = $isPostingRestrictedToMods;
$dto->localSubscribers = $localSubscribers;
$dto->discoverable = $discoverable;
+ $dto->indexable = $indexable;
return $dto;
}
@@ -131,6 +134,7 @@ public function jsonSerialize(): mixed
'localSubscribers' => $this->localSubscribers,
'notificationStatus' => $this->notificationStatus,
'discoverable' => $this->discoverable,
+ 'indexable' => $this->indexable,
];
}
}
diff --git a/src/DTO/MagazineSmallResponseDto.php b/src/DTO/MagazineSmallResponseDto.php
index feb1adf6c9..7a1a5af5a2 100644
--- a/src/DTO/MagazineSmallResponseDto.php
+++ b/src/DTO/MagazineSmallResponseDto.php
@@ -18,6 +18,7 @@ class MagazineSmallResponseDto implements \JsonSerializable
public ?string $apId = null;
public ?string $apProfileId = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
public function __construct(MagazineDto $dto)
{
@@ -30,6 +31,7 @@ public function __construct(MagazineDto $dto)
$this->apId = $dto->apId;
$this->apProfileId = $dto->apProfileId;
$this->discoverable = $dto->discoverable;
+ $this->indexable = $dto->indexable;
}
public function jsonSerialize(): mixed
@@ -44,6 +46,7 @@ public function jsonSerialize(): mixed
'apId' => $this->apId,
'apProfileId' => $this->apProfileId,
'discoverable' => $this->discoverable,
+ 'indexable' => $this->indexable,
];
}
}
diff --git a/src/DTO/MagazineUpdateRequestDto.php b/src/DTO/MagazineUpdateRequestDto.php
index 56655cd4dc..53b24d8a30 100644
--- a/src/DTO/MagazineUpdateRequestDto.php
+++ b/src/DTO/MagazineUpdateRequestDto.php
@@ -17,6 +17,7 @@ class MagazineUpdateRequestDto
public ?bool $isAdult = null;
public ?bool $isPostingRestrictedToMods = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
public function mergeIntoDto(MagazineDto $dto, ImageRepository $imageRepository): MagazineDto
{
@@ -27,6 +28,7 @@ public function mergeIntoDto(MagazineDto $dto, ImageRepository $imageRepository)
$dto->isAdult = null === $this->isAdult ? $this->isAdult : $dto->isAdult;
$dto->isPostingRestrictedToMods = $this->isPostingRestrictedToMods ?? false;
$dto->discoverable = $this->discoverable ?? $dto->discoverable ?? true;
+ $dto->indexable = $this->indexable ?? $dto->indexable ?? true;
return $dto;
}
diff --git a/src/DTO/UserDto.php b/src/DTO/UserDto.php
index 2dd477c022..299c740136 100644
--- a/src/DTO/UserDto.php
+++ b/src/DTO/UserDto.php
@@ -52,6 +52,7 @@ class UserDto implements UserDtoInterface
public ?string $applicationText = null;
public ?int $reputationPoints = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
#[Assert\Callback]
public function validate(
@@ -97,6 +98,7 @@ public static function create(
?string $applicationText = null,
?int $reputationPoints = null,
?bool $discoverable = null,
+ ?bool $indexable = null,
): self {
$dto = new UserDto();
$dto->id = $id;
@@ -116,6 +118,7 @@ public static function create(
$dto->applicationText = $applicationText;
$dto->reputationPoints = $reputationPoints;
$dto->discoverable = $discoverable;
+ $dto->indexable = $indexable;
return $dto;
}
diff --git a/src/DTO/UserResponseDto.php b/src/DTO/UserResponseDto.php
index 3ab6521c33..dc3b522aa2 100644
--- a/src/DTO/UserResponseDto.php
+++ b/src/DTO/UserResponseDto.php
@@ -28,6 +28,7 @@ class UserResponseDto implements \JsonSerializable
public ?string $serverSoftware = null;
public ?string $serverSoftwareVersion = null;
public ?ENotificationStatus $notificationStatus = null;
+ public ?bool $indexable = null;
/**
* @var int|null this will only be populated on single user retrieves, not on batch ones,
@@ -57,6 +58,7 @@ public function __construct(UserDto $dto)
$this->isGlobalModerator = $dto->isGlobalModerator;
$this->reputationPoints = $dto->reputationPoints;
$this->discoverable = $dto->discoverable;
+ $this->indexable = $dto->indexable;
}
public function jsonSerialize(): mixed
@@ -82,6 +84,7 @@ public function jsonSerialize(): mixed
'notificationStatus' => $this->notificationStatus,
'reputationPoints' => $this->reputationPoints,
'discoverable' => $this->discoverable,
+ 'indexable' => $this->indexable,
];
}
}
diff --git a/src/DTO/UserSettingsDto.php b/src/DTO/UserSettingsDto.php
index 6642b18af8..50e069606f 100644
--- a/src/DTO/UserSettingsDto.php
+++ b/src/DTO/UserSettingsDto.php
@@ -44,6 +44,7 @@ public function __construct(
#[OA\Property(type: 'string', enum: EFrontContentOptions::OPTIONS)]
public ?string $frontDefaultContent = null,
public ?bool $discoverable = null,
+ public ?bool $indexable = null,
) {
}
@@ -72,6 +73,7 @@ public function jsonSerialize(): mixed
'notifyOnUserSignup' => $this->notifyOnUserSignup,
'directMessageSetting' => $this->directMessageSetting,
'discoverable' => $this->discoverable,
+ 'indexable' => $this->indexable,
];
}
@@ -98,6 +100,7 @@ public function mergeIntoDto(UserSettingsDto $dto): UserSettingsDto
$dto->directMessageSetting = $this->directMessageSetting ?? $dto->directMessageSetting;
$dto->frontDefaultContent = $this->frontDefaultContent ?? $dto->frontDefaultContent;
$dto->discoverable = $this->discoverable ?? $dto->discoverable;
+ $dto->indexable = $this->indexable ?? $dto->indexable;
return $dto;
}
diff --git a/src/DTO/UserSmallResponseDto.php b/src/DTO/UserSmallResponseDto.php
index d6d0739e7b..b8a2d39fe9 100644
--- a/src/DTO/UserSmallResponseDto.php
+++ b/src/DTO/UserSmallResponseDto.php
@@ -22,6 +22,7 @@ class UserSmallResponseDto implements \JsonSerializable
public ?string $apProfileId = null;
public ?\DateTimeImmutable $createdAt = null;
public ?bool $discoverable = null;
+ public ?bool $indexable = null;
public function __construct(UserDto $dto)
{
@@ -38,6 +39,7 @@ public function __construct(UserDto $dto)
$this->isAdmin = $dto->isAdmin;
$this->isGlobalModerator = $dto->isGlobalModerator;
$this->discoverable = $dto->discoverable;
+ $this->indexable = $dto->indexable;
}
public function jsonSerialize(): mixed
@@ -56,6 +58,7 @@ public function jsonSerialize(): mixed
'apProfileId' => $this->apProfileId,
'createdAt' => $this->createdAt?->format(\DateTimeImmutable::ATOM),
'discoverable' => $this->discoverable,
+ 'indexable' => $this->indexable,
];
}
}
diff --git a/src/Entity/Traits/ActivityPubActorTrait.php b/src/Entity/Traits/ActivityPubActorTrait.php
index 703819904f..746fc7e846 100644
--- a/src/Entity/Traits/ActivityPubActorTrait.php
+++ b/src/Entity/Traits/ActivityPubActorTrait.php
@@ -42,6 +42,9 @@ trait ActivityPubActorTrait
#[Column(type: 'boolean', nullable: true)]
public ?bool $apDiscoverable = null;
+ #[Column(type: 'boolean', nullable: true)]
+ public ?bool $apIndexable = null;
+
#[Column(type: 'boolean', nullable: true)]
public ?bool $apManuallyApprovesFollowers = null;
diff --git a/src/Factory/ActivityPub/GroupFactory.php b/src/Factory/ActivityPub/GroupFactory.php
index d2ed4b556d..892ad898e7 100644
--- a/src/Factory/ActivityPub/GroupFactory.php
+++ b/src/Factory/ActivityPub/GroupFactory.php
@@ -75,6 +75,7 @@ public function create(Magazine $magazine, bool $includeContext = true): array
),
'postingRestrictedToMods' => $magazine->postingRestrictedToMods,
'discoverable' => $magazine->apDiscoverable,
+ 'indexable' => $magazine->apIndexable,
'endpoints' => [
'sharedInbox' => $this->urlGenerator->generate(
'ap_shared_inbox',
diff --git a/src/Factory/ActivityPub/PersonFactory.php b/src/Factory/ActivityPub/PersonFactory.php
index 8911f91160..8a5439bf5f 100644
--- a/src/Factory/ActivityPub/PersonFactory.php
+++ b/src/Factory/ActivityPub/PersonFactory.php
@@ -46,6 +46,7 @@ public function create(User $user, bool $context = true): array
'url' => $this->getActivityPubId($user),
'manuallyApprovesFollowers' => false,
'discoverable' => $user->apDiscoverable,
+ 'indexable' => $user->apIndexable,
'published' => $user->createdAt->format(DATE_ATOM),
'following' => $this->urlGenerator->generate(
'ap_user_following',
diff --git a/src/Factory/MagazineFactory.php b/src/Factory/MagazineFactory.php
index e66e0d4312..dc4e9b3f8b 100644
--- a/src/Factory/MagazineFactory.php
+++ b/src/Factory/MagazineFactory.php
@@ -71,6 +71,7 @@ public function createDto(Magazine $magazine): MagazineDto
$dto->isAdult = $magazine->isAdult;
$dto->isPostingRestrictedToMods = $magazine->postingRestrictedToMods;
$dto->discoverable = $magazine->apDiscoverable;
+ $dto->indexable = $magazine->apIndexable;
$dto->tags = $magazine->tags;
$dto->badges = $magazine->badges;
$dto->moderators = $magazine->moderators;
@@ -178,6 +179,7 @@ public function createResponseDto(MagazineDto|Magazine $magazine): MagazineRespo
$dto->isPostingRestrictedToMods,
$dto->localSubscribers,
$dto->discoverable,
+ $dto->indexable,
);
}
diff --git a/src/Factory/UserFactory.php b/src/Factory/UserFactory.php
index 287aa7b4ea..b0e711927a 100644
--- a/src/Factory/UserFactory.php
+++ b/src/Factory/UserFactory.php
@@ -41,6 +41,7 @@ public function createDto(User $user, ?int $reputationPoints = null): UserDto
$currentUser && ($currentUser->isAdmin() || $currentUser->isModerator()) ? $user->applicationText : null,
reputationPoints: $reputationPoints,
discoverable: $user->apDiscoverable,
+ indexable: $user->apIndexable,
);
// Only return the user's vote if permission to control voting has been given
diff --git a/src/Form/MagazineType.php b/src/Form/MagazineType.php
index 1fb4e58619..a416f947c8 100644
--- a/src/Form/MagazineType.php
+++ b/src/Form/MagazineType.php
@@ -29,6 +29,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'required' => false,
'help' => 'magazine_discoverable_help',
])
+ ->add('indexable', CheckboxType::class, [
+ 'required' => false,
+ 'help' => 'magazine_indexable_by_search_engines_help',
+ ])
->add('submit', SubmitType::class);
$builder->addEventSubscriber(new DisableFieldsOnMagazineEdit());
diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php
index 2652099957..3137a03f49 100644
--- a/src/Form/UserSettingsType.php
+++ b/src/Form/UserSettingsType.php
@@ -86,6 +86,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'required' => false,
'help' => 'user_discoverable_help',
])
+ ->add('indexable', CheckboxType::class, [
+ 'required' => false,
+ 'help' => 'user_indexable_by_search_engines_help',
+ ])
->add('featuredMagazines', TextareaType::class, ['required' => false])
->add('preferredLanguages', LanguageType::class, [
'required' => false,
diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php
index 391e311911..2c8f6d749d 100644
--- a/src/Service/ActivityPubManager.php
+++ b/src/Service/ActivityPubManager.php
@@ -384,6 +384,7 @@ private function updateUser(string $actorUrl): ?User
$user->apAttributedToUrl = $actor['attributedTo'] ?? null;
$user->apPreferredUsername = $actor['preferredUsername'] ?? null;
$user->apDiscoverable = $actor['discoverable'] ?? null;
+ $user->apIndexable = $actor['indexable'] ?? null;
$user->apManuallyApprovesFollowers = $actor['manuallyApprovesFollowers'] ?? false;
$user->apPublicUrl = $actor['url'] ?? $actorUrl;
$user->apDeletedAt = null;
@@ -656,6 +657,7 @@ private function updateMagazine(string $actorUrl): ?Magazine
$magazine->apFetchedAt = new \DateTime();
$magazine->isAdult = $actor['sensitive'] ?? false;
$magazine->postingRestrictedToMods = filter_var($actor['postingRestrictedToMods'] ?? false, FILTER_VALIDATE_BOOLEAN) ?? false;
+ $magazine->apIndexable = $actor['indexable'] ?? null;
if (null !== $magazine->apFollowersUrl) {
try {
diff --git a/src/Service/MagazineManager.php b/src/Service/MagazineManager.php
index 683d159962..2aafab5ead 100644
--- a/src/Service/MagazineManager.php
+++ b/src/Service/MagazineManager.php
@@ -84,6 +84,8 @@ public function create(MagazineDto $dto, ?User $user, bool $rateLimit = true): M
);
// default new local magazines to be discoverable
$magazine->apDiscoverable = $dto->discoverable ?? true;
+ // default new local magazines to be indexable
+ $magazine->apIndexable = $dto->indexable ?? true;
}
$this->entityManager->persist($magazine);
@@ -149,6 +151,9 @@ public function edit(Magazine $magazine, MagazineDto $dto, User $editedBy): Maga
if (null !== $dto->discoverable) {
$magazine->apDiscoverable = $dto->discoverable;
}
+ if (null !== $dto->indexable) {
+ $magazine->apIndexable = $dto->indexable;
+ }
$this->entityManager->flush();
diff --git a/src/Service/UserManager.php b/src/Service/UserManager.php
index ea936430dc..9c84fbd607 100644
--- a/src/Service/UserManager.php
+++ b/src/Service/UserManager.php
@@ -172,6 +172,8 @@ public function create(UserDto $dto, bool $verifyUserEmail = true, $rateLimit =
$user = KeysGenerator::generate($user);
// default new local users to be discoverable
$user->apDiscoverable = $dto->discoverable ?? true;
+ // default new local users to be indexable
+ $user->apIndexable = true;
}
$this->entityManager->persist($user);
diff --git a/src/Service/UserSettingsManager.php b/src/Service/UserSettingsManager.php
index 25ff2fe2e8..41dad85270 100644
--- a/src/Service/UserSettingsManager.php
+++ b/src/Service/UserSettingsManager.php
@@ -39,6 +39,7 @@ public function createDto(User $user): UserSettingsDto
$user->directMessageSetting,
$user->frontDefaultContent,
$user->apDiscoverable,
+ $user->apIndexable,
);
}
@@ -73,6 +74,10 @@ public function update(User $user, UserSettingsDto $dto): void
$user->apDiscoverable = $dto->discoverable;
}
+ if (null !== $dto->indexable) {
+ $user->apIndexable = $dto->indexable;
+ }
+
$this->entityManager->flush();
}
}
diff --git a/templates/admin/federation.html.twig b/templates/admin/federation.html.twig
index 1b354245f1..2d2079d45d 100644
--- a/templates/admin/federation.html.twig
+++ b/templates/admin/federation.html.twig
@@ -30,7 +30,9 @@
{{ form_label(form.federationUsesAllowList, 'federation_uses_allowlist') }}
{{ form_widget(form.federationUsesAllowList) }}
- {{ form_help(form.federationUsesAllowList) }}
+
+ {{ form_help(form.federationUsesAllowList) }}
+
{{ form_row(form.submit, { 'label': 'save'|trans, attr: {class: 'btn btn__primary'} }) }}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 1399707eee..7de34bc37a 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -35,6 +35,16 @@
+ {% if magazine is defined and magazine and magazine.apIndexable is same as false %}
+
+ {% elseif user is defined and user and user.apIndexable is same as false %}
+
+ {% elseif entry is defined and entry and entry.user and entry.user.apIndexable is same as false %}
+
+ {% elseif post is defined and post and post.user and post.user.apIndexable is same as false %}
+
+ {% endif %}
+
diff --git a/templates/magazine/panel/general.html.twig b/templates/magazine/panel/general.html.twig
index 4ed905f652..ade7a99019 100644
--- a/templates/magazine/panel/general.html.twig
+++ b/templates/magazine/panel/general.html.twig
@@ -51,6 +51,13 @@
{{ form_help(form.discoverable) }}
+
+ {{ form_label(form.indexable, 'indexable_by_search_engines') }}
+ {{ form_widget(form.indexable) }}
+
+
+ {{ form_help(form.indexable) }}
+
{{ form_row(form.submit, { 'label': 'done'|trans, 'attr': {'class': 'btn btn__primary'} }) }}
diff --git a/templates/user/settings/general.html.twig b/templates/user/settings/general.html.twig
index dd86b0f904..49e18e8853 100644
--- a/templates/user/settings/general.html.twig
+++ b/templates/user/settings/general.html.twig
@@ -45,6 +45,13 @@
{{ form_help(form.discoverable) }}
+
+ {{ form_label(form.indexable, 'indexable_by_search_engines') }}
+ {{ form_widget(form.indexable) }}
+
+
+ {{ form_help(form.indexable) }}
+
{{ 'notifications'|trans }}
{{ form_row(form.notifyOnNewEntryReply, {label: 'notify_on_new_entry_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewEntryCommentReply, {label: 'notify_on_new_entry_comment_reply', row_attr: {class: 'checkbox'}}) }}
diff --git a/tests/FactoryTrait.php b/tests/FactoryTrait.php
index ee3422b66a..dac981edf0 100644
--- a/tests/FactoryTrait.php
+++ b/tests/FactoryTrait.php
@@ -100,6 +100,7 @@ private function createUser(string $username, ?string $email = null, ?string $pa
$user->hideAdult = $hideAdult;
$user->apDiscoverable = true;
$user->about = $about;
+ $user->apIndexable = true;
if ($addImage) {
$user->avatar = $this->createImage(bin2hex(random_bytes(20)).'.png');
}
diff --git a/tests/Functional/Controller/Api/Instance/InstanceRetrieveInfoApiTest.php b/tests/Functional/Controller/Api/Instance/InstanceRetrieveInfoApiTest.php
index b3fddec074..1ec20824f5 100644
--- a/tests/Functional/Controller/Api/Instance/InstanceRetrieveInfoApiTest.php
+++ b/tests/Functional/Controller/Api/Instance/InstanceRetrieveInfoApiTest.php
@@ -38,6 +38,7 @@ class InstanceRetrieveInfoApiTest extends WebTestCase
'endpoints',
'icon',
'discoverable',
+ 'indexable',
];
public function testCanRetrieveInfoAnonymous(): void
diff --git a/tests/Functional/Controller/Api/Magazine/Admin/MagazineCreateApiTest.php b/tests/Functional/Controller/Api/Magazine/Admin/MagazineCreateApiTest.php
index fd6c165c87..cb1ce8ef51 100644
--- a/tests/Functional/Controller/Api/Magazine/Admin/MagazineCreateApiTest.php
+++ b/tests/Functional/Controller/Api/Magazine/Admin/MagazineCreateApiTest.php
@@ -52,6 +52,8 @@ public function testApiCanCreateMagazine(): void
'rules' => $rules,
'isAdult' => false,
'discoverable' => false,
+ 'isPostingRestrictedToMods' => true,
+ 'indexable' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
@@ -67,6 +69,8 @@ public function testApiCanCreateMagazine(): void
self::assertEquals($rules, $jsonData['rules']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['discoverable']);
+ self::assertTrue($jsonData['isPostingRestrictedToMods']);
+ self::assertFalse($jsonData['indexable']);
}
public function testApiCannotCreateInvalidMagazine(): void
diff --git a/tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateApiTest.php b/tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateApiTest.php
index 2e99db00df..dee528460c 100644
--- a/tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateApiTest.php
+++ b/tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateApiTest.php
@@ -4,7 +4,6 @@
namespace App\Tests\Functional\Controller\Api\Magazine\Admin;
-use App\Tests\Functional\Controller\Api\Magazine\MagazineRetrieveApiTest;
use App\Tests\WebTestCase;
class MagazineUpdateApiTest extends WebTestCase
@@ -57,6 +56,7 @@ public function testApiCanUpdateMagazine(): void
'rules' => $rules,
'isAdult' => true,
'discoverable' => false,
+ 'indexable' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
@@ -65,13 +65,14 @@ public function testApiCanUpdateMagazine(): void
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
- self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
+ self::assertArrayKeysMatch(WebTestCase::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertEquals($name, $jsonData['name']);
self::assertSame($user->getId(), $jsonData['owner']['userId']);
self::assertEquals($description, $jsonData['description']);
self::assertEquals($rules, $jsonData['rules']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['discoverable']);
+ self::assertFalse($jsonData['indexable']);
}
public function testApiCannotUpdateMagazineWithInvalidParams(): void
diff --git a/tests/Functional/Controller/Api/User/UserRetrieveApiTest.php b/tests/Functional/Controller/Api/User/UserRetrieveApiTest.php
index 804a5d4bb2..514b72eac0 100644
--- a/tests/Functional/Controller/Api/User/UserRetrieveApiTest.php
+++ b/tests/Functional/Controller/Api/User/UserRetrieveApiTest.php
@@ -32,6 +32,7 @@ class UserRetrieveApiTest extends WebTestCase
'directMessageSetting',
'frontDefaultContent',
'discoverable',
+ 'indexable',
];
public const NUM_USERS = 10;
diff --git a/tests/Functional/Controller/Api/User/UserUpdateApiTest.php b/tests/Functional/Controller/Api/User/UserUpdateApiTest.php
index 655d8c6856..2e63b128a3 100644
--- a/tests/Functional/Controller/Api/User/UserUpdateApiTest.php
+++ b/tests/Functional/Controller/Api/User/UserUpdateApiTest.php
@@ -137,6 +137,7 @@ public function testApiCanUpdateCurrentUserSettings(): void
directMessageSetting: EDirectMessageSettings::FollowersOnly->value,
frontDefaultContent: EFrontContentOptions::Threads->value,
discoverable: false,
+ indexable: false,
))->jsonSerialize();
$this->client->jsonRequest(
@@ -170,6 +171,7 @@ public function testApiCanUpdateCurrentUserSettings(): void
self::assertEquals(['en'], $jsonData['preferredLanguages']);
self::assertEquals(EDirectMessageSettings::FollowersOnly->value, $jsonData['directMessageSetting']);
self::assertEquals(EFrontContentOptions::Threads->value, $jsonData['frontDefaultContent']);
+ self::assertFalse($jsonData['indexable']);
$this->client->request('GET', '/api/users/settings', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
@@ -198,5 +200,6 @@ public function testApiCanUpdateCurrentUserSettings(): void
self::assertEquals(['en'], $jsonData['preferredLanguages']);
self::assertEquals(EDirectMessageSettings::FollowersOnly->value, $jsonData['directMessageSetting']);
self::assertEquals(EFrontContentOptions::Threads->value, $jsonData['frontDefaultContent']);
+ self::assertFalse($jsonData['indexable']);
}
}
diff --git a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateMagazine__1.json b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateMagazine__1.json
index 4512bee39a..c772713c0f 100644
--- a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateMagazine__1.json
+++ b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateMagazine__1.json
@@ -37,6 +37,7 @@
"attributedTo": "https://kbin.test/m/test/moderators",
"postingRestrictedToMods": false,
"discoverable": true,
+ "indexable": true,
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
},
diff --git a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateUser__1.json b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateUser__1.json
index 9d8783eee3..9434f379f0 100644
--- a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateUser__1.json
+++ b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateUser__1.json
@@ -27,6 +27,7 @@
"url": "https://kbin.test/u/user",
"manuallyApprovesFollowers": false,
"discoverable": true,
+ "indexable": true,
"published": "SCRUBBED_DATE",
"following": "https://kbin.test/u/user/following",
"followers": "https://kbin.test/u/user/followers",
diff --git a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryWithUrlAndImage__1.json b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryWithUrlAndImage__1.json
index 0dbbb10f6b..836efc5d8e 100644
--- a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryWithUrlAndImage__1.json
+++ b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryWithUrlAndImage__1.json
@@ -50,7 +50,7 @@
{
"type": "Image",
"mediaType": "image/png",
- "url": "https://mbin.domain.tld/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png",
+ "url": "https://kbin.test/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png",
"name": "kibby",
"blurhash": "L$Pie*?^spxt%3W.oyn*r^W-tQjG",
"focalPoint": [
@@ -67,7 +67,7 @@
],
"image": {
"type": "Image",
- "url": "https://mbin.domain.tld/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png"
+ "url": "https://kbin.test/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png"
}
},
"audience": "https://kbin.test/m/test"
diff --git a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateMagazine__1.json b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateMagazine__1.json
index 76aa31654b..1c21918cb8 100644
--- a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateMagazine__1.json
+++ b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateMagazine__1.json
@@ -33,6 +33,7 @@
"attributedTo": "https://kbin.test/m/test/moderators",
"postingRestrictedToMods": false,
"discoverable": true,
+ "indexable": true,
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
},
diff --git a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateUser__1.json b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateUser__1.json
index c38e375816..2a89db2534 100644
--- a/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateUser__1.json
+++ b/tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateUser__1.json
@@ -23,6 +23,7 @@
"url": "https://kbin.test/u/user",
"manuallyApprovesFollowers": false,
"discoverable": true,
+ "indexable": true,
"published": "SCRUBBED_DATE",
"following": "https://kbin.test/u/user/following",
"followers": "https://kbin.test/u/user/followers",
diff --git a/tests/WebTestCase.php b/tests/WebTestCase.php
index 9e248cef3b..5e07478875 100644
--- a/tests/WebTestCase.php
+++ b/tests/WebTestCase.php
@@ -86,16 +86,16 @@ abstract class WebTestCase extends BaseWebTestCase
protected const PAGINATION_KEYS = ['count', 'currentPage', 'maxPage', 'perPage'];
protected const IMAGE_KEYS = ['filePath', 'sourceUrl', 'storageUrl', 'altText', 'width', 'height', 'blurHash'];
protected const MESSAGE_RESPONSE_KEYS = ['messageId', 'threadId', 'sender', 'body', 'status', 'createdAt'];
- protected const USER_RESPONSE_KEYS = ['userId', 'username', 'about', 'avatar', 'cover', 'createdAt', 'followersCount', 'apId', 'apProfileId', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'isAdmin', 'isGlobalModerator', 'serverSoftware', 'serverSoftwareVersion', 'notificationStatus', 'reputationPoints', 'discoverable'];
- protected const USER_SMALL_RESPONSE_KEYS = ['userId', 'username', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'avatar', 'apId', 'apProfileId', 'createdAt', 'isAdmin', 'isGlobalModerator', 'discoverable'];
+ protected const USER_RESPONSE_KEYS = ['userId', 'username', 'about', 'avatar', 'cover', 'createdAt', 'followersCount', 'apId', 'apProfileId', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'isAdmin', 'isGlobalModerator', 'serverSoftware', 'serverSoftwareVersion', 'notificationStatus', 'reputationPoints', 'discoverable', 'indexable'];
+ protected const USER_SMALL_RESPONSE_KEYS = ['userId', 'username', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'avatar', 'apId', 'apProfileId', 'createdAt', 'isAdmin', 'isGlobalModerator', 'discoverable', 'indexable'];
protected const ENTRY_RESPONSE_KEYS = ['entryId', 'magazine', 'user', 'domain', 'title', 'url', 'image', 'body', 'lang', 'tags', 'badges', 'numComments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'isOc', 'isAdult', 'isPinned', 'isLocked', 'createdAt', 'editedAt', 'lastActive', 'visibility', 'type', 'slug', 'apId', 'canAuthUserModerate', 'notificationStatus', 'bookmarks', 'crosspostedEntries', 'isAuthorModeratorInMagazine'];
protected const ENTRY_COMMENT_RESPONSE_KEYS = ['commentId', 'magazine', 'user', 'entryId', '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 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 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'];
- protected const MAGAZINE_SMALL_RESPONSE_KEYS = ['magazineId', 'name', 'icon', 'banner', 'isUserSubscribed', 'isBlockedByUser', 'apId', 'apProfileId', 'discoverable'];
+ 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'];
protected const KIBBY_PNG_URL_RESULT = 'a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png';
diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml
index 973c037093..7c06805e67 100644
--- a/translations/messages.en.yaml
+++ b/translations/messages.en.yaml
@@ -1129,3 +1129,10 @@ modlog_type_post_unlock: Microblog unlocked
contentnotification.muted: Mute | get no notifications
contentnotification.default: Default | get notifications according to your default settings
contentnotification.loud: Loud | get all notifications
+indexable_by_search_engines: Indexable by search engines
+user_indexable_by_search_engines_help: If this setting is false, search engines are advised to not index any of your threads
+ and microblogs, however your comments are not affected by this and bad actors might ignore it. This setting is also
+ federated to other servers.
+magazine_indexable_by_search_engines_help: If this setting is false, search engines are advised to not index any of the
+ threads and microblogs in this magazines. That includes the landing page and all comment pages. This setting is also
+ federated to other servers.