From 85bd770304f01fb62aff8bc4640f2d635a1601cd Mon Sep 17 00:00:00 2001 From: BentiGorlich Date: Sun, 13 Oct 2024 17:36:04 +0200 Subject: [PATCH] Make related panels respect blocks (#1183) --- src/Repository/BookmarkRepository.php | 28 ++------- src/Repository/EntryRepository.php | 44 ++++++++++---- src/Repository/MagazineRepository.php | 39 +++++++----- src/Repository/PostRepository.php | 48 +++++++++++---- .../Components/RelatedEntriesComponent.php | 19 ++++-- .../Components/RelatedMagazinesComponent.php | 15 +++-- src/Twig/Components/RelatedPostsComponent.php | 19 ++++-- src/Utils/SqlHelpers.php | 59 +++++++++++++++++++ 8 files changed, 193 insertions(+), 78 deletions(-) create mode 100644 src/Utils/SqlHelpers.php diff --git a/src/Repository/BookmarkRepository.php b/src/Repository/BookmarkRepository.php index 39cd79c1c..e8982f4c6 100644 --- a/src/Repository/BookmarkRepository.php +++ b/src/Repository/BookmarkRepository.php @@ -14,6 +14,7 @@ use App\Pagination\NativeQueryAdapter; use App\Pagination\Pagerfanta; use App\Pagination\Transformation\ContentPopulationTransformer; +use App\Utils\SqlHelpers; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; @@ -130,10 +131,10 @@ public function findPopulatedByList(BookmarkList $list, Criteria $criteria, ?int $parameters['time'] = $criteria->getSince(); } - $entryWhere = $this->makeWhereString($entryWhereArr); - $entryCommentWhere = $this->makeWhereString($entryCommentWhereArr); - $postWhere = $this->makeWhereString($postWhereArr); - $postCommentWhere = $this->makeWhereString($postCommentWhereArr); + $entryWhere = SqlHelpers::makeWhereString($entryWhereArr); + $entryCommentWhere = SqlHelpers::makeWhereString($entryCommentWhereArr); + $postWhere = SqlHelpers::makeWhereString($postWhereArr); + $postCommentWhere = SqlHelpers::makeWhereString($postCommentWhereArr); $sql = " SELECT * FROM ( @@ -158,23 +159,4 @@ public function findPopulatedByList(BookmarkList $list, Criteria $criteria, ?int return Pagerfanta::createForCurrentPageWithMaxPerPage($adapter, $criteria->page, $perPage ?? EntryRepository::PER_PAGE); } - - private function makeWhereString(array $whereClauses): string - { - if (empty($whereClauses)) { - return ''; - } - - $where = 'WHERE '; - $i = 0; - foreach ($whereClauses as $whereClause) { - if ($i > 0) { - $where .= ' AND '; - } - $where .= $whereClause; - ++$i; - } - - return $where; - } } diff --git a/src/Repository/EntryRepository.php b/src/Repository/EntryRepository.php index 2dfcbe7e8..cc7540e2f 100644 --- a/src/Repository/EntryRepository.php +++ b/src/Repository/EntryRepository.php @@ -24,6 +24,7 @@ use App\PageView\EntryPageView; use App\Pagination\AdapterFactory; use App\Service\SettingsManager; +use App\Utils\SqlHelpers; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Types\Types; @@ -56,6 +57,7 @@ public function __construct( private readonly CacheInterface $cache, private readonly AdapterFactory $adapterFactory, private readonly SettingsManager $settingsManager, + private readonly SqlHelpers $sqlHelpers ) { parent::__construct($registry, Entry::class); } @@ -151,6 +153,7 @@ private function addBannedHashtagClause(QueryBuilder $qb): void private function filter(QueryBuilder $qb, EntryPageView $criteria): QueryBuilder { + /** @var User $user */ $user = $this->security->getUser(); if (Criteria::AP_LOCAL === $criteria->federation) { @@ -339,12 +342,11 @@ public function findToDelete(User $user, int $limit): array ->getResult(); } - public function findRelatedByTag(string $tag, ?int $limit = 1): array + public function findRelatedByTag(string $tag, ?int $limit = 1, ?User $user = null): array { $qb = $this->createQueryBuilder('e'); - return $qb - ->andWhere('e.visibility = :visibility') + $qb->andWhere('e.visibility = :visibility') ->andWhere('m.visibility = :visibility') ->andWhere('u.visibility = :visibility') ->andWhere('u.isDeleted = false') @@ -360,16 +362,23 @@ public function findRelatedByTag(string $tag, ?int $limit = 1): array 'visibility' => VisibilityInterface::VISIBILITY_VISIBLE, 'tag' => $tag, ]) - ->setMaxResults($limit) - ->getQuery() + ->setMaxResults($limit); + + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + + return $qb->getQuery() ->getResult(); } - public function findRelatedByMagazine(string $name, ?int $limit = 1): array + public function findRelatedByMagazine(string $name, ?int $limit = 1, ?User $user = null): array { $qb = $this->createQueryBuilder('e'); - return $qb->where('m.name LIKE :name OR m.title LIKE :title') + $qb->where('m.name LIKE :name OR m.title LIKE :title') ->andWhere('e.visibility = :visibility') ->andWhere('m.visibility = :visibility') ->andWhere('u.visibility = :visibility') @@ -382,12 +391,19 @@ public function findRelatedByMagazine(string $name, ?int $limit = 1): array ->setParameters( ['name' => "%{$name}%", 'title' => "%{$name}%", 'visibility' => VisibilityInterface::VISIBILITY_VISIBLE] ) - ->setMaxResults($limit) - ->getQuery() + ->setMaxResults($limit); + + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + + return $qb->getQuery() ->getResult(); } - public function findLast(int $limit): array + public function findLast(int $limit, ?User $user = null): array { $qb = $this->createQueryBuilder('e'); @@ -401,10 +417,16 @@ public function findLast(int $limit): array $qb = $qb->andWhere('m.apId IS NULL'); } + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + return $qb->join('e.magazine', 'm') ->join('e.user', 'u') ->orderBy('e.createdAt', 'DESC') - ->setParameters(['visibility' => VisibilityInterface::VISIBILITY_VISIBLE]) + ->setParameter('visibility', VisibilityInterface::VISIBILITY_VISIBLE) ->setMaxResults($limit) ->getQuery() ->getResult(); diff --git a/src/Repository/MagazineRepository.php b/src/Repository/MagazineRepository.php index 0f42034f4..ebd92131e 100644 --- a/src/Repository/MagazineRepository.php +++ b/src/Repository/MagazineRepository.php @@ -16,6 +16,7 @@ use App\Entity\User; use App\PageView\MagazinePageView; use App\Service\SettingsManager; +use App\Utils\SqlHelpers; use App\Utils\SubscriptionSort; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\Collection; @@ -49,7 +50,7 @@ class MagazineRepository extends ServiceEntityRepository self::SORT_NEWEST, ]; - public function __construct(ManagerRegistry $registry, private readonly SettingsManager $settingsManager) + public function __construct(ManagerRegistry $registry, private readonly SettingsManager $settingsManager, private readonly SqlHelpers $sqlHelpers) { parent::__construct($registry, Magazine::class); } @@ -478,21 +479,23 @@ public function search(string $magazine, int $page, int $perPage = self::PER_PAG return $pagerfanta; } - public function findRandom(): array + public function findRandom(?User $user = null): array { $conn = $this->getEntityManager()->getConnection(); - $sql = ' - SELECT id FROM magazine - '; + $whereClauses = []; + $parameters = []; if ($this->settingsManager->get('MBIN_SIDEBAR_SECTIONS_LOCAL_ONLY')) { - $sql .= 'WHERE ap_id IS NULL'; + $whereClauses[] = 'm.ap_id IS NULL'; + } + if (null !== $user) { + $subSql = 'SELECT * FROM magazine_block mb WHERE mb.magazine_id = m.id AND mb.user_id = :user'; + $whereClauses[] = "NOT EXISTS($subSql)"; + $parameters['user'] = $user->getId(); } - $sql .= ' - ORDER BY random() - LIMIT 5 - '; + $whereString = SqlHelpers::makeWhereString($whereClauses); + $sql = "SELECT m.id FROM magazine m $whereString ORDER BY random() LIMIT 5"; $stmt = $conn->prepare($sql); - $stmt = $stmt->executeQuery(); + $stmt = $stmt->executeQuery($parameters); $ids = $stmt->fetchAllAssociative(); return $this->createQueryBuilder('m') @@ -505,17 +508,23 @@ public function findRandom(): array ->getResult(); } - public function findRelated(string $magazine): array + public function findRelated(string $magazine, ?User $user = null): array { - return $this->createQueryBuilder('m') + $qb = $this->createQueryBuilder('m') ->where('m.entryCount > 0 OR m.postCount > 0') ->andWhere('m.title LIKE :magazine OR m.description LIKE :magazine OR m.name LIKE :magazine') ->andWhere('m.isAdult = false') ->andWhere('m.visibility = :visibility') ->setParameter('visibility', VisibilityInterface::VISIBILITY_VISIBLE) ->setParameter('magazine', "%{$magazine}%") - ->setMaxResults(5) - ->getQuery() + ->setMaxResults(5); + + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))); + $qb->setParameter('user', $user); + } + + return $qb->getQuery() ->getResult(); } diff --git a/src/Repository/PostRepository.php b/src/Repository/PostRepository.php index 870b20490..380e7470d 100644 --- a/src/Repository/PostRepository.php +++ b/src/Repository/PostRepository.php @@ -23,6 +23,7 @@ use App\PageView\PostPageView; use App\Pagination\AdapterFactory; use App\Service\SettingsManager; +use App\Utils\SqlHelpers; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Types\Types; @@ -54,6 +55,7 @@ public function __construct( private readonly CacheInterface $cache, private readonly AdapterFactory $adapterFactory, private readonly SettingsManager $settingsManager, + private readonly SqlHelpers $sqlHelpers, ) { parent::__construct($registry, Post::class); } @@ -143,6 +145,7 @@ private function addBannedHashtagClause(QueryBuilder $qb): void private function filter(QueryBuilder $qb, Criteria $criteria): QueryBuilder { + /** @var User|null $user */ $user = $this->security->getUser(); if (Criteria::AP_LOCAL === $criteria->federation) { @@ -168,8 +171,8 @@ private function filter(QueryBuilder $qb, Criteria $criteria): QueryBuilder if ($criteria->subscribed) { $qb->andWhere( - 'EXISTS (SELECT IDENTITY(ms.magazine) FROM '.MagazineSubscription::class.' ms WHERE ms.user = :user AND ms.magazine = p.magazine) - OR + 'EXISTS (SELECT IDENTITY(ms.magazine) FROM '.MagazineSubscription::class.' ms WHERE ms.user = :user AND ms.magazine = p.magazine) + OR EXISTS (SELECT IDENTITY(uf.following) FROM '.UserFollow::class.' uf WHERE uf.follower = :user AND uf.following = p.user) OR p.user = :user' @@ -307,11 +310,11 @@ public function findToDelete(User $user, int $limit): array ->getResult(); } - public function findRelatedByTag(string $tag, ?int $limit = 1): array + public function findRelatedByTag(string $tag, ?int $limit = 1, ?User $user = null): array { $qb = $this->createQueryBuilder('p'); - return $qb + $qb = $qb ->andWhere('p.visibility = :visibility') ->andWhere('m.visibility = :visibility') ->andWhere('u.visibility = :visibility') @@ -328,16 +331,23 @@ public function findRelatedByTag(string $tag, ?int $limit = 1): array 'visibility' => VisibilityInterface::VISIBILITY_VISIBLE, 'name' => $tag, ]) - ->setMaxResults($limit) - ->getQuery() + ->setMaxResults($limit); + + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + + return $qb->getQuery() ->getResult(); } - public function findRelatedByMagazine(string $name, ?int $limit = 1): array + public function findRelatedByMagazine(string $name, ?int $limit = 1, ?User $user = null): array { $qb = $this->createQueryBuilder('p'); - return $qb->where('m.name LIKE :name OR m.title LIKE :title') + $qb = $qb->where('m.name LIKE :name OR m.title LIKE :title') ->andWhere('p.visibility = :visibility') ->andWhere('m.visibility = :visibility') ->andWhere('u.visibility = :visibility') @@ -349,12 +359,19 @@ public function findRelatedByMagazine(string $name, ?int $limit = 1): array ->setParameters( ['name' => "%{$name}%", 'title' => "%{$name}%", 'visibility' => VisibilityInterface::VISIBILITY_VISIBLE] ) - ->setMaxResults($limit) - ->getQuery() + ->setMaxResults($limit); + + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + + return $qb->getQuery() ->getResult(); } - public function findLast(int $limit = 1): array + public function findLast(int $limit = 1, ?User $user = null): array { $qb = $this->createQueryBuilder('p'); @@ -365,9 +382,16 @@ public function findLast(int $limit = 1): array $qb = $qb->andWhere('m.apId IS NULL'); } + if (null !== $user) { + $qb->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedMagazinesDql($user)))) + ->andWhere($qb->expr()->not($qb->expr()->exists($this->sqlHelpers->getBlockedUsersDql($user)))); + $qb->setParameter('user', $user); + } + return $qb->join('p.magazine', 'm') + ->join('p.user', 'u') ->orderBy('p.createdAt', 'DESC') - ->setParameters(['visibility' => VisibilityInterface::VISIBILITY_VISIBLE]) + ->setParameter('visibility', VisibilityInterface::VISIBILITY_VISIBLE) ->setMaxResults($limit) ->getQuery() ->getResult(); diff --git a/src/Twig/Components/RelatedEntriesComponent.php b/src/Twig/Components/RelatedEntriesComponent.php index febe94156..4615006c3 100644 --- a/src/Twig/Components/RelatedEntriesComponent.php +++ b/src/Twig/Components/RelatedEntriesComponent.php @@ -5,9 +5,11 @@ namespace App\Twig\Components; use App\Entity\Entry; +use App\Entity\User; use App\Repository\EntryRepository; use App\Service\MentionManager; use App\Service\SettingsManager; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -31,7 +33,8 @@ public function __construct( private readonly EntryRepository $repository, private readonly CacheInterface $cache, private readonly SettingsManager $settingsManager, - private readonly MentionManager $mentionManager + private readonly MentionManager $mentionManager, + private readonly Security $security, ) { } @@ -49,19 +52,23 @@ public function mount(?string $magazine, ?string $tag): void $entryId = $this->entry?->getId(); $magazine = str_replace('@', '', $magazine ?? ''); + /** @var User|null $user */ + $user = $this->security->getUser(); + $cacheKey = "related_entries_{$magazine}_{$tag}_{$entryId}_{$this->type}_{$this->settingsManager->getLocale()}_{$user?->getId()}"; $entryIds = $this->cache->get( - "related_entries_{$magazine}_{$tag}_{$entryId}_{$this->type}_{$this->settingsManager->getLocale()}", - function (ItemInterface $item) use ($magazine, $tag) { + $cacheKey, + function (ItemInterface $item) use ($magazine, $tag, $user) { $item->expiresAfter(60 * 5); // 5 minutes $entries = match ($this->type) { - self::TYPE_TAG => $this->repository->findRelatedByMagazine($tag, $this->limit + 20), + self::TYPE_TAG => $this->repository->findRelatedByMagazine($tag, $this->limit + 20, user: $user), self::TYPE_MAGAZINE => $this->repository->findRelatedByTag( $this->mentionManager->getUsername($magazine), - $this->limit + 20 + $this->limit + 20, + user: $user, ), - default => $this->repository->findLast($this->limit + 150), + default => $this->repository->findLast($this->limit + 150, user: $user), }; $entries = array_filter($entries, fn (Entry $e) => !$e->isAdult && !$e->magazine->isAdult); diff --git a/src/Twig/Components/RelatedMagazinesComponent.php b/src/Twig/Components/RelatedMagazinesComponent.php index 741dea67f..0871cda57 100644 --- a/src/Twig/Components/RelatedMagazinesComponent.php +++ b/src/Twig/Components/RelatedMagazinesComponent.php @@ -5,8 +5,10 @@ namespace App\Twig\Components; use App\Entity\Magazine; +use App\Entity\User; use App\Repository\MagazineRepository; use App\Service\SettingsManager; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -28,6 +30,7 @@ public function __construct( private readonly MagazineRepository $repository, private readonly CacheInterface $cache, private readonly SettingsManager $settingsManager, + private readonly Security $security, ) { } @@ -44,16 +47,18 @@ public function mount(?string $magazine, ?string $tag): void } $magazine = str_replace('@', '', $magazine ?? ''); + /** @var User|null $user */ + $user = $this->security->getUser(); $magazineIds = $this->cache->get( - "related_magazines_{$magazine}_{$tag}_{$this->type}_{$this->settingsManager->getLocale()}", - function (ItemInterface $item) use ($magazine, $tag) { + "related_magazines_{$magazine}_{$tag}_{$this->type}_{$this->settingsManager->getLocale()}_{$user?->getId()}", + function (ItemInterface $item) use ($magazine, $tag, $user) { $item->expiresAfter(60 * 5); // 5 minutes $magazines = match ($this->type) { - self::TYPE_TAG => $this->repository->findRelated($tag), - self::TYPE_MAGAZINE => $this->repository->findRelated($magazine), - default => $this->repository->findRandom(), + self::TYPE_TAG => $this->repository->findRelated($tag, user: $user), + self::TYPE_MAGAZINE => $this->repository->findRelated($magazine, user: $user), + default => $this->repository->findRandom(user: $user), }; $magazines = array_filter($magazines, fn ($m) => $m->name !== $magazine); diff --git a/src/Twig/Components/RelatedPostsComponent.php b/src/Twig/Components/RelatedPostsComponent.php index bfc1d944d..a5a033950 100644 --- a/src/Twig/Components/RelatedPostsComponent.php +++ b/src/Twig/Components/RelatedPostsComponent.php @@ -5,9 +5,11 @@ namespace App\Twig\Components; use App\Entity\Post; +use App\Entity\User; use App\Repository\PostRepository; use App\Service\MentionManager; use App\Service\SettingsManager; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -30,7 +32,8 @@ public function __construct( private readonly PostRepository $repository, private readonly CacheInterface $cache, private readonly SettingsManager $settingsManager, - private readonly MentionManager $mentionManager + private readonly MentionManager $mentionManager, + private readonly Security $security, ) { } @@ -46,21 +49,25 @@ public function mount(?string $magazine, ?string $tag): void $this->type = self::TYPE_MAGAZINE; } + /** @var User|null $user */ + $user = $this->security->getUser(); + $postId = $this->post?->getId(); $magazine = str_replace('@', '', $magazine ?? ''); $postIds = $this->cache->get( - "related_posts_{$magazine}_{$tag}_{$postId}_{$this->type}_{$this->settingsManager->getLocale()}", - function (ItemInterface $item) use ($magazine, $tag) { + "related_posts_{$magazine}_{$tag}_{$postId}_{$this->type}_{$this->settingsManager->getLocale()}_{$user?->getId()}", + function (ItemInterface $item) use ($magazine, $tag, $user) { $item->expiresAfter(60 * 5); // 5 minutes $posts = match ($this->type) { - self::TYPE_TAG => $this->repository->findRelatedByMagazine($tag, $this->limit + 20), + self::TYPE_TAG => $this->repository->findRelatedByMagazine($tag, $this->limit + 20, user: $user), self::TYPE_MAGAZINE => $this->repository->findRelatedByTag( $this->mentionManager->getUsername($magazine), - $this->limit + 20 + $this->limit + 20, + user: $user ), - default => $this->repository->findLast($this->limit + 150), + default => $this->repository->findLast($this->limit + 150, user: $user), }; $posts = array_filter($posts, fn (Post $p) => !$p->isAdult && !$p->magazine->isAdult); diff --git a/src/Utils/SqlHelpers.php b/src/Utils/SqlHelpers.php new file mode 100644 index 000000000..39e9de186 --- /dev/null +++ b/src/Utils/SqlHelpers.php @@ -0,0 +1,59 @@ + 0) { + $where .= ' AND '; + } + $where .= $whereClause; + ++$i; + } + + return $where; + } + + public function getBlockedMagazinesDql(User $user): string + { + return $this->entityManager->createQueryBuilder() + ->select('bm') + ->from(MagazineBlock::class, 'bm') + ->where('bm.magazine = m') + ->andWhere('bm.user = :user') + ->setParameter('user', $user) + ->getDQL(); + } + + public function getBlockedUsersDql(User $user): string + { + return $this->entityManager->createQueryBuilder() + ->select('ub') + ->from(UserBlock::class, 'ub') + ->where('ub.blocker = :user') + ->andWhere('ub.blocked = u') + ->setParameter('user', $user) + ->getDql(); + } +}