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.