From ae10a2b5d8ec30d9883f2c09252a3cb9cdb1f06d Mon Sep 17 00:00:00 2001
From: Sybille Peters <sypets@gmx.de>
Date: Mon, 23 Sep 2019 18:44:18 +0200
Subject: [PATCH] [FEATURE] Show broken links only in editable fields

Linkvalidator should show broken links only in the list of broken links,
if current backend user has edit access to the field. This way
the editor will no longer get an error message on trying to
edit records he has no permission to edit.

Whether the editor has access depends on a number of factors.

We check the following:

* The current permissions of the page. For editing the page, the editor
  must have Permission::PAGE_EDIT, for editing content
  Permission::CONTENT_EDIT must be available
* The user has write access to the table. We check if the table
  is in 'tables_modify' for the group(s)
* The user has write access to the field. We check if the field
  is an exclude field. If yes, it must be included in
  'non_exclude_fields' for the group(s).
* The user has write permission for the language of the record
* For all tables with type fields: The type is in list of explicitly
  allowed values for authMode (or not explicitly denied depending on
  the setting).

Resolves: #84214
Releases: master
Change-Id: Iade53d0452e0a5dec98e9d5b7b149d137f170949
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61786
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Sybille Peters <sypets@gmx.de>
Tested-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Sybille Peters <sypets@gmx.de>
Reviewed-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
---
 ...eckIfFieldsAreEditableForLinkvalidator.rst |  42 +
 .../linkvalidator/Classes/LinkAnalyzer.php    |  24 +-
 .../Classes/Linktype/InternalLinktype.php     |   4 +-
 .../QueryRestrictions/EditableRestriction.php | 231 ++++++
 .../Classes/Report/LinkValidatorReport.php    |  19 +-
 .../Repository/BrokenLinkRepository.php       |  46 +-
 .../Classes/Task/ValidatorTask.php            |  11 +-
 .../Repository/BrokenLinkRepositoryTest.php   | 723 ++++++++++++++++++
 .../Repository/Fixtures/be_groups.xml         |  35 +
 .../Repository/Fixtures/be_users.xml          |  75 ++
 .../Functional/Repository/Fixtures/input.xml  |  36 +
 .../Fixtures/input_permissions_group.xml      |  35 +
 .../Fixtures/input_permissions_user_2.xml     |  35 +
 .../Fixtures/input_permissions_user_3.xml     |  41 +
 .../Fixtures/input_permissions_user_4.xml     |  41 +
 .../Fixtures/input_permissions_user_5.xml     |  29 +
 ...nput_permissions_user_6_explicit_allow.xml |  26 +
 typo3/sysext/linkvalidator/ext_tables.sql     |   2 +
 18 files changed, 1429 insertions(+), 26 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-84214-AddCheckIfFieldsAreEditableForLinkvalidator.rst
 create mode 100644 typo3/sysext/linkvalidator/Classes/QueryRestrictions/EditableRestriction.php
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/BrokenLinkRepositoryTest.php
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_group.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_2.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_3.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_4.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_5.xml
 create mode 100644 typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_6_explicit_allow.xml

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84214-AddCheckIfFieldsAreEditableForLinkvalidator.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84214-AddCheckIfFieldsAreEditableForLinkvalidator.rst
new file mode 100644
index 000000000000..b8386ce16e97
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-84214-AddCheckIfFieldsAreEditableForLinkvalidator.rst
@@ -0,0 +1,42 @@
+.. include:: ../../Includes.txt
+
+====================================================================
+Feature: #84214 - Add check if fields are editable for Linkvalidator
+====================================================================
+
+See :issue:`84214`
+
+Description
+===========
+
+Broken links should only be shown in the list of broken links,
+if current backend user has edit access to the field. This way
+the editor will no longer get an error message on trying to
+edit records he has no permission to edit.
+
+Whether the editor has access depends on a number of factors.
+
+We check the following:
+
+* The current permissions of the page. For editing the page, the editor must have Permission::PAGE_EDIT, for editing content Permission::CONTENT_EDIT must be available
+* The user has write access to the table. We check if the table
+  is in 'tables_modify' for the group(s)
+* The user has write access to the field. We check if the field
+  is an exclude field. If yes, it must be included in
+  'non_exclude_fields' for the group(s).
+* The user has write permission for the language of the record
+* For tt_content: The CType is in list of explicitly allowed
+  values for authMode.
+
+Impact
+======
+
+* Broken links for fields that are not editable for the current backend
+  user will no longer be shown.
+* Fields were added to the tx_linkvalidator_link table. "Analyze
+  Database Structure" must be executed.
+* After an update to the new version, checking of broken links should
+  be reinitiated for the entire site. Until this is done, some broken
+  links may not be displayed for editors in the broken link report.
+
+.. index:: Backend, ext:linkvalidator
diff --git a/typo3/sysext/linkvalidator/Classes/LinkAnalyzer.php b/typo3/sysext/linkvalidator/Classes/LinkAnalyzer.php
index 90c7ac12c269..7d368915512c 100644
--- a/typo3/sysext/linkvalidator/Classes/LinkAnalyzer.php
+++ b/typo3/sysext/linkvalidator/Classes/LinkAnalyzer.php
@@ -158,6 +158,12 @@ class LinkAnalyzer
 
             // Re-init selectFields for table
             $selectFields = array_merge(['uid', 'pid', $GLOBALS['TCA'][$table]['ctrl']['label']], $fields);
+            if ($GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false) {
+                $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
+            }
+            if ($GLOBALS['TCA'][$table]['ctrl']['type'] ?? false) {
+                $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['type'];
+            }
 
             $result = $queryBuilder->select(...$selectFields)
                 ->from($table)
@@ -196,6 +202,16 @@ class LinkAnalyzer
                 $record['link_title'] = $entryValue['link_title'];
                 $record['field'] = $entryValue['field'];
                 $record['last_check'] = time();
+                $typeField = $GLOBALS['TCA'][$table]['ctrl']['type'] ?? false;
+                if ($entryValue['row'][$typeField] ?? false) {
+                    $record['element_type'] = $entryValue['row'][$typeField];
+                }
+                $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false;
+                if ($languageField && isset($entryValue['row'][$languageField])) {
+                    $record['language'] = $entryValue['row'][$languageField];
+                } else {
+                    $record['language'] = -1;
+                }
                 $this->recordReference = $entryValue['substr']['recordRef'];
                 if (!empty($entryValue['pageAndAnchor'] ?? '')) {
                     // Page with anchor, e.g. 18#1580
@@ -462,13 +478,7 @@ class LinkAnalyzer
      */
     public function getLinkCounts()
     {
-        $groupedResult = $this->brokenLinkRepository->getNumberOfBrokenLinksForRecordsOnPages($this->pids);
-        $data = [];
-        foreach ($groupedResult as $linkType => $amount) {
-            $data[$linkType] = $amount;
-            $data['brokenlinkCount'] += $amount;
-        }
-        return $data;
+        return $this->brokenLinkRepository->getNumberOfBrokenLinksForRecordsOnPages($this->pids, $this->searchFields);
     }
 
     /**
diff --git a/typo3/sysext/linkvalidator/Classes/Linktype/InternalLinktype.php b/typo3/sysext/linkvalidator/Classes/Linktype/InternalLinktype.php
index 4708aaae57f0..e438979edc73 100644
--- a/typo3/sysext/linkvalidator/Classes/Linktype/InternalLinktype.php
+++ b/typo3/sysext/linkvalidator/Classes/Linktype/InternalLinktype.php
@@ -105,8 +105,8 @@ class InternalLinktype extends AbstractLinktype
             $this->responseContent = $this->checkContent($page, $anchor);
         }
         if (
-            is_array($this->errorParams['page']) && !$this->responsePage
-            || is_array($this->errorParams['content']) && !$this->responseContent
+            (is_array($this->errorParams['page']) && !$this->responsePage)
+            || (is_array($this->errorParams['content']) && !$this->responseContent)
         ) {
             $this->setErrorParams($this->errorParams);
         }
diff --git a/typo3/sysext/linkvalidator/Classes/QueryRestrictions/EditableRestriction.php b/typo3/sysext/linkvalidator/Classes/QueryRestrictions/EditableRestriction.php
new file mode 100644
index 000000000000..9d6a6d1138a9
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Classes/QueryRestrictions/EditableRestriction.php
@@ -0,0 +1,231 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Linkvalidator\QueryRestrictions;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryHelper;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
+
+class EditableRestriction implements QueryRestrictionInterface
+{
+    /**
+     * Specify which database fields the current user is allowed to edit
+     *
+     * @var array
+     */
+    protected $allowedFields = [];
+
+    /**
+     * Specify which languages the current user is allowed to edit
+     *
+     * @var array
+     */
+    protected $allowedLanguages = [];
+
+    /**
+     * Explicit allow fields
+     *
+     * @var array
+     */
+    protected $explicitAllowFields = [];
+
+    /**
+     * @var QueryBuilder
+     */
+    protected $queryBuilder;
+
+    /**
+     * @param array $searchFields array of 'table' => 'field1, field2'
+     *   in which linkvalidator searches for broken links.
+     * @param QueryBuilder $queryBuilder
+     */
+    public function __construct(array $searchFields, QueryBuilder $queryBuilder)
+    {
+        $this->allowedFields = $this->getAllowedFieldsForCurrentUser($searchFields);
+        $this->allowedLanguages = $this->getAllowedLanguagesForCurrentUser();
+        foreach ($searchFields as $table => $fields) {
+            if ($table !== 'pages' && ($GLOBALS['TCA'][$table]['ctrl']['type'] ?? false)) {
+                $type = $GLOBALS['TCA'][$table]['ctrl']['type'];
+                $this->explicitAllowFields[$table][$type] = $this->getExplicitAllowFieldsForCurrentUser($table, $type);
+            }
+        }
+        $this->queryBuilder = $queryBuilder;
+    }
+
+    /**
+     * Gets all allowed language ids for current backend user
+     *
+     * @return array
+     */
+    protected function getAllowedLanguagesForCurrentUser(): array
+    {
+        if (!(is_string($GLOBALS['BE_USER']->groupData['allowed_languages'] ?? false))) {
+            return [];
+        }
+
+        return array_map('intval', explode(',', $GLOBALS['BE_USER']->groupData['allowed_languages']));
+    }
+
+    protected function getExplicitAllowFieldsForCurrentUser(string $table, string $field): array
+    {
+        $allowDenyOptions = [];
+        $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
+        // Check for items
+        if ($fieldConfig['type'] === 'select' && is_array($fieldConfig['items'] ?? false)) {
+            foreach ($fieldConfig['items'] as $iVal) {
+                $itemIdentifier = (string)$iVal[1];
+                if ($GLOBALS['BE_USER']->checkAuthMode($table, $field, $itemIdentifier, $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'])) {
+                    $allowDenyOptions[] = $itemIdentifier;
+                }
+            }
+        }
+        return $allowDenyOptions;
+    }
+
+    /**
+     * Get allowed table / fieldnames for current backend user.
+     * Only consider table / fields in $searchFields
+     *
+     * @param array $searchFields array of 'table' => ['field1, field2', ....]
+     *   in which linkvalidator searches for broken links
+     * @return array
+     */
+    protected function getAllowedFieldsForCurrentUser(array $searchFields = []): array
+    {
+        if (!$searchFields) {
+            return [];
+        }
+
+        $allowedFields = [];
+
+        foreach ($searchFields as $table => $fieldList) {
+            if (!$GLOBALS['BE_USER']->isAdmin() && !$GLOBALS['BE_USER']->check('tables_modify', $table)) {
+                // table not allowed
+                continue;
+            }
+            foreach ($fieldList as $field) {
+                $isExcludeField = $GLOBALS['TCA'][$table]['columns'][$field]['exclude'] ?? false;
+                if (!$GLOBALS['BE_USER']->isAdmin()
+                    && $isExcludeField
+                    && !$GLOBALS['BE_USER']->check('non_exclude_fields', $table . ':' . $field)) {
+                    continue;
+                }
+                $allowedFields[$table][$field] = true;
+            }
+        }
+        return $allowedFields;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+
+        if ($this->allowedFields) {
+            $constraints = [
+                $expressionBuilder->orX(
+                // broken link is in page and page is editable
+                    $expressionBuilder->andX(
+                        $expressionBuilder->eq(
+                            'tx_linkvalidator_link.table_name',
+                            $this->queryBuilder->createNamedParameter('pages')
+                        ),
+                        QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_EDIT))
+                    ),
+                    // OR broken link is in content and content is editable
+                    $expressionBuilder->andX(
+                        $expressionBuilder->neq(
+                            'tx_linkvalidator_link.table_name',
+                            $this->queryBuilder->createNamedParameter('pages')
+                        ),
+                        QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(Permission::CONTENT_EDIT))
+                    )
+                )
+            ];
+
+            // check if fields are editable
+            $additionalWhere = [];
+            foreach ($this->allowedFields as $table => $fields) {
+                foreach ($fields as $field => $value) {
+                    $additionalWhere[] = $expressionBuilder->andX(
+                        $expressionBuilder->eq(
+                            'tx_linkvalidator_link.table_name',
+                            $this->queryBuilder->createNamedParameter($table)
+                        ),
+                        $expressionBuilder->eq(
+                            'tx_linkvalidator_link.field',
+                            $this->queryBuilder->createNamedParameter($field)
+                        )
+                    );
+                }
+            }
+            if ($additionalWhere) {
+                $constraints[] = $expressionBuilder->orX(...$additionalWhere);
+            }
+        } else {
+            // add a constraint that will always return zero records because there are NO allowed fields
+            $constraints[] = $expressionBuilder->isNull('tx_linkvalidator_link.table_name');
+        }
+
+        foreach ($this->explicitAllowFields as $table => $field) {
+            $additionalWhere = [];
+            $additionalWhere[] = $expressionBuilder->andX(
+                $expressionBuilder->eq(
+                    'tx_linkvalidator_link.table_name',
+                    $this->queryBuilder->createNamedParameter($table)
+                ),
+                $expressionBuilder->in(
+                    'tx_linkvalidator_link.element_type',
+                    $this->queryBuilder->createNamedParameter(
+                        array_unique(current($field)),
+                        Connection::PARAM_STR_ARRAY
+                    )
+                )
+            );
+            $additionalWhere[] = $expressionBuilder->neq(
+                'tx_linkvalidator_link.table_name',
+                $this->queryBuilder->createNamedParameter($table)
+            );
+            if ($additionalWhere) {
+                $constraints[] = $expressionBuilder->orX(...$additionalWhere);
+            }
+        }
+
+        if ($this->allowedLanguages) {
+            $additionalWhere = [];
+            foreach ($this->allowedLanguages as $langId) {
+                $additionalWhere[] = $expressionBuilder->orX(
+                    $expressionBuilder->eq(
+                        'tx_linkvalidator_link.language',
+                        $this->queryBuilder->createNamedParameter($langId, \PDO::PARAM_INT)
+                    ),
+                    $expressionBuilder->eq(
+                        'tx_linkvalidator_link.language',
+                        $this->queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
+                    )
+                );
+            }
+            $constraints[] = $expressionBuilder->orX(...$additionalWhere);
+        }
+        // If allowed languages is empty: all languages are allowed, so no constraint in this case
+
+        return $expressionBuilder->andX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/linkvalidator/Classes/Report/LinkValidatorReport.php b/typo3/sysext/linkvalidator/Classes/Report/LinkValidatorReport.php
index 724312865614..a66d17d8bdbe 100644
--- a/typo3/sysext/linkvalidator/Classes/Report/LinkValidatorReport.php
+++ b/typo3/sysext/linkvalidator/Classes/Report/LinkValidatorReport.php
@@ -143,6 +143,11 @@ class LinkValidatorReport
      */
     protected $view;
 
+    public function __construct()
+    {
+        $this->brokenLinkRepository = GeneralUtility::makeInstance(BrokenLinkRepository::class);
+    }
+
     /**
      * Init, called from parent object
      *
@@ -262,8 +267,8 @@ class LinkValidatorReport
             } else {
                 // mark broken links for last edited record as needing a recheck
                 $this->brokenLinkRepository->setNeedsRecheckForRecord(
-                    $this->lastEditedRecord['table'],
-                    (int)$this->lastEditedRecord['uid']
+                    (int)$this->lastEditedRecord['uid'],
+                    $this->lastEditedRecord['table']
                 );
             }
         }
@@ -324,7 +329,7 @@ class LinkValidatorReport
         }
 
         $this->pageRecord = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
-        if ($this->id && is_array($this->pageRecord) || !$this->id && $this->getBackendUser()->isAdmin()) {
+        if (($this->id && is_array($this->pageRecord)) || (!$this->id && $this->getBackendUser()->isAdmin())) {
             $this->isAccessibleForCurrentUser = true;
         }
         // Don't access in workspace
@@ -402,7 +407,11 @@ class LinkValidatorReport
         $items = [];
         $rootLineHidden = $this->linkAnalyzer->getRootLineIsHidden($this->pObj->pageinfo);
         if (!$rootLineHidden || (bool)$this->modTS['checkhidden'] && !empty($linkTypes)) {
-            $brokenLinks = $this->brokenLinkRepository->getAllBrokenLinksForPages($this->getPageList(), $linkTypes);
+            $brokenLinks = $this->brokenLinkRepository->getAllBrokenLinksForPages(
+                $this->getPageList(),
+                $linkTypes,
+                $this->searchFields
+            );
             foreach ($brokenLinks as $row) {
                 $items[] = $this->renderTableRow($row['table_name'], $row);
             }
@@ -570,7 +579,7 @@ class LinkValidatorReport
     {
         $variables = [];
         $variables['totalCountLabel'] = BackendUtility::wrapInHelp('linkvalidator', 'checkboxes', $this->getLanguageService()->getLL('overviews.nbtotal'));
-        $variables['totalCount'] = $brokenLinkOverView['brokenlinkCount'] ?: '0';
+        $variables['totalCount'] = $brokenLinkOverView['total'] ?: '0';
         $variables['optionsByType'] = [];
         $linkTypes = GeneralUtility::trimExplode(',', $this->modTS['linktypes'] ?? '', true);
         $availableLinkTypes = array_keys($this->hookObjectsArr);
diff --git a/typo3/sysext/linkvalidator/Classes/Repository/BrokenLinkRepository.php b/typo3/sysext/linkvalidator/Classes/Repository/BrokenLinkRepository.php
index 36bf60b38079..38c85c01dffc 100644
--- a/typo3/sysext/linkvalidator/Classes/Repository/BrokenLinkRepository.php
+++ b/typo3/sysext/linkvalidator/Classes/Repository/BrokenLinkRepository.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Linkvalidator\Repository;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Linkvalidator\QueryRestrictions\EditableRestriction;
 
 /**
  * Repository for finding broken links that were detected previously.
@@ -90,17 +91,27 @@ class BrokenLinkRepository
      * grouped by the link_type.
      *
      * @param array $pageIds
+     * @param array $searchFields [ table => [field1, field2, ...], ...]
      * @return array
      */
-    public function getNumberOfBrokenLinksForRecordsOnPages(array $pageIds): array
+    public function getNumberOfBrokenLinksForRecordsOnPages(array $pageIds, array $searchFields): array
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
             ->getQueryBuilderForTable(static::TABLE);
         $queryBuilder->getRestrictions()->removeAll();
-
+        if (!$GLOBALS['BE_USER']->isAdmin()) {
+            $queryBuilder->getRestrictions()
+                ->add(GeneralUtility::makeInstance(EditableRestriction::class, $searchFields, $queryBuilder));
+        }
         $statement = $queryBuilder->select('link_type')
-            ->addSelectLiteral($queryBuilder->expr()->count('uid', 'amount'))
+            ->addSelectLiteral($queryBuilder->expr()->count(static::TABLE . '.uid', 'amount'))
             ->from(static::TABLE)
+            ->join(
+                static::TABLE,
+                'pages',
+                'pages',
+                $queryBuilder->expr()->eq('record_pid', $queryBuilder->quoteIdentifier('pages.uid'))
+            )
             ->where(
                 $queryBuilder->expr()->orX(
                     $queryBuilder->expr()->andX(
@@ -122,9 +133,12 @@ class BrokenLinkRepository
             ->groupBy('link_type')
             ->execute();
 
-        $result = [];
+        $result = [
+            'total' => 0
+        ];
         while ($row = $statement->fetch()) {
             $result[$row['link_type']] = $row['amount'];
+            $result['total']+= $row['amount'];
         }
         return $result;
     }
@@ -205,17 +219,31 @@ class BrokenLinkRepository
     /**
      * Prepare database query with pageList and keyOpt data.
      *
+     * This takes permissions of current BE user into account
+     *
      * @param int[] $pageIds Pages to check for broken links
      * @param string[] $linkTypes Link types to validate
+     * @param string[] $searchFields table => [fields1, field2, ...], ... : fields in which linkvalidator should
+     *   search for broken links
      * @return array
      */
-    public function getAllBrokenLinksForPages(array $pageIds, array $linkTypes): array
+    public function getAllBrokenLinksForPages(array $pageIds, array $linkTypes, array $searchFields = []): array
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
             ->getQueryBuilderForTable(self::TABLE);
+        if (!$GLOBALS['BE_USER']->isAdmin()) {
+            $queryBuilder->getRestrictions()
+                ->add(GeneralUtility::makeInstance(EditableRestriction::class, $searchFields, $queryBuilder));
+        }
         $records = $queryBuilder
-            ->select('*')
+            ->select(self::TABLE . '.*')
             ->from(self::TABLE)
+            ->join(
+                'tx_linkvalidator_link',
+                'pages',
+                'pages',
+                $queryBuilder->expr()->eq('tx_linkvalidator_link.record_pid', $queryBuilder->quoteIdentifier('pages.uid'))
+            )
             ->where(
                 $queryBuilder->expr()->orX(
                     $queryBuilder->expr()->andX(
@@ -238,13 +266,13 @@ class BrokenLinkRepository
                     $queryBuilder->createNamedParameter($linkTypes, Connection::PARAM_STR_ARRAY)
                 )
             )
-            ->orderBy('record_uid')
-            ->addOrderBy('uid')
+            ->orderBy('tx_linkvalidator_link.record_uid')
+            ->addOrderBy('tx_linkvalidator_link.uid')
             ->execute()
             ->fetchAll();
         foreach ($records as &$record) {
             $response = json_decode($record['url_response'], true);
-            // Fallback mechansim to still support the old serialized data, could be removed in TYPO3 v12 or later
+            // Fallback mechanism to still support the old serialized data, could be removed in TYPO3 v12 or later
             if ($response === null) {
                 $response = unserialize($record['url_response'], ['allowed_classes' => false]);
             }
diff --git a/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php b/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
index ef246de3329c..30d9362d5173 100644
--- a/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
+++ b/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MailUtility;
 use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
+use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;
 use TYPO3\CMS\Scheduler\Task\AbstractTask;
 
 /**
@@ -128,6 +129,9 @@ class ValidatorTask extends AbstractTask
      */
     protected $languageFile = 'LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf';
 
+    /** @var BrokenLinkRepository */
+    protected $brokenLinkRepository;
+
     /**
      * Get the value of the protected property email
      *
@@ -256,6 +260,7 @@ class ValidatorTask extends AbstractTask
      */
     public function execute()
     {
+        $this->brokenLinkRepository = GeneralUtility::makeInstance(BrokenLinkRepository::class);
         $this->setCliArguments();
         $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
         $successfullyExecuted = true;
@@ -343,12 +348,12 @@ class ValidatorTask extends AbstractTask
             $processor->init($searchFields, $pageIds, $modTs);
             if (!empty($this->email)) {
                 $oldLinkCounts = $processor->getLinkCounts();
-                $this->oldTotalBrokenLink += $oldLinkCounts['brokenlinkCount'];
+                $this->oldTotalBrokenLink += $oldLinkCounts['total'];
             }
             $processor->getLinkStatistics($linkTypes, $modTs['checkhidden']);
             if (!empty($this->email)) {
                 $linkCounts = $processor->getLinkCounts();
-                $this->totalBrokenLink += $linkCounts['brokenlinkCount'];
+                $this->totalBrokenLink += $linkCounts['total'];
                 $pageSections = $this->buildMail($page, $pageIds, $linkCounts, $oldLinkCounts);
             }
         }
@@ -547,7 +552,7 @@ class ValidatorTask extends AbstractTask
             BackendUtility::getRecord('pages', $curPage)
         );
         $content = '';
-        if ($markerArray['brokenlinkCount'] > 0) {
+        if ($markerArray['total'] > 0) {
             $content = $this->templateService->substituteMarkerArray(
                 $pageSectionHtml,
                 $markerArray,
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/BrokenLinkRepositoryTest.php b/typo3/sysext/linkvalidator/Tests/Functional/Repository/BrokenLinkRepositoryTest.php
new file mode 100644
index 000000000000..77fafb17e68e
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/BrokenLinkRepositoryTest.php
@@ -0,0 +1,723 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Linkvalidator\Tests\Functional\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
+use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class BrokenLinkRepositoryTest extends FunctionalTestCase
+{
+    /**
+     * @var array
+     */
+    protected $coreExtensionsToLoad = [
+        'linkvalidator',
+        'seo'
+    ];
+
+    /**
+     * @var BrokenLinkRepository
+     */
+    protected $brokenLinksRepository;
+
+    protected $beusers = [
+        'admin' => [
+            'fixture' => 'PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/be_users.xml',
+            'uid' => 1,
+            'groupFixture' => ''
+        ],
+        'no group' => [
+            'fixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml',
+            'uid' => 2,
+            'groupFixture' => ''
+        ],
+        // write access to pages, tt_content
+        'group 1' => [
+            'fixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml',
+            'uid' => 3,
+            'groupFixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml'
+        ],
+        // write access to pages, tt_content, exclude field pages.header_link
+        'group 2' => [
+            'fixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml',
+            'uid' => 4,
+            'groupFixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml'
+        ],
+        // write access to pages, tt_content (restricted to default language)
+        'group 3' => [
+            'fixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml',
+            'uid' => 5,
+            'groupFixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml'
+        ],
+        // group 6: access to all, but restricted via explicit allow to CType=texmedia and text
+        'group 6' => [
+            'fixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml',
+            'uid' => 6,
+            'groupFixture' => 'EXT:linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml'
+        ],
+
+    ];
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        Bootstrap::initializeLanguageObject();
+
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'] = 'explicitAllow';
+        $this->brokenLinksRepository = new BrokenLinkRepository();
+    }
+
+    public function getLinkCountsForPagesAndLinktypesReturnsCorrectCountForUserDataProvider()
+    {
+        yield 'Admin user should see all broken links' =>
+        [
+            // backendUser: 1=admin
+            $this->beusers['admin'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'db' => 1,
+                'file' => 1,
+                'external' => 2,
+                'total' => 4,
+            ]
+        ];
+        yield 'User with no group should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'total' => 0,
+            ]
+        ];
+        yield 'User with permission to pages but not to specific tables should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_2.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'total' => 0,
+            ]
+        ];
+        yield 'User with permission to pages and to specific tables, but no exclude fields should see 3 of 4 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 1'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_3.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'db' => 1,
+                'file' => 1,
+                'external' => 1,
+                'total' => 3
+            ]
+        ];
+        yield 'User with permission to pages, specific tables and exclude fields should see all broken links' =>
+        [
+            // backend user
+            $this->beusers['group 2'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_4.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'db' => 1,
+                'file' => 1,
+                'external' => 2,
+                'total' => 4
+            ]
+        ];
+        yield 'User has write permission only for Ctype textmedia and text, should see only broken links from textmedia records' =>
+        [
+            // backend user
+            $this->beusers['group 6'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_6_explicit_allow.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'external' => 1,
+                'total' => 1
+            ]
+        ];
+
+        yield 'User has write permission only for default language and should see only 1 of 2 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 3'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_5.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                'external' => 1,
+                'total' => 1
+            ]
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider getLinkCountsForPagesAndLinktypesReturnsCorrectCountForUserDataProvider
+     */
+    public function getLinkCountsForPagesAndLinktypesReturnsCorrectCountForUser(
+        array $beuser,
+        string $inputFile,
+        array $pidList,
+        array $expectedOutput
+    ) {
+        $config = [
+            'db' => '1',
+            'file' => '1',
+            'external' => '1',
+            'linkhandler' => '1'
+
+        ];
+
+        $tsConfig = [
+            'searchFields.' => [
+                'pages' => 'media,url,canonical_link',
+                'tt_content' => 'bodytext,header_link,records'
+            ],
+            'linktypes' => 'db,file,external,linkhandler',
+            'checkhidden' => '0',
+            'linkhandler' => [
+                'reportHiddenRecords' => '0'
+            ]
+        ];
+
+        $searchFields = $tsConfig['searchFields.'];
+        foreach ($searchFields as $table => $fields) {
+            $searchFields[$table] = explode(',', $fields);
+        }
+
+        $this->setupBackendUser($beuser['uid'], $beuser['fixture'], $beuser['groupFixture']);
+
+        $this->importDataSet($inputFile);
+
+        $linkAnalyzer = new LinkAnalyzer(
+            $this->prophesize(EventDispatcherInterface::class)->reveal(),
+            $this->brokenLinksRepository
+        );
+        $linkAnalyzer->init($searchFields, implode(',', $pidList), $tsConfig);
+        $linkAnalyzer->getLinkStatistics($config);
+        $result = $this->brokenLinksRepository->getNumberOfBrokenLinksForRecordsOnPages(
+            $pidList,
+            $searchFields
+        );
+
+        self::assertEquals($expectedOutput, $result);
+    }
+
+    public function getAllBrokenLinksForPagesReturnsCorrectCountForUserDataProvider()
+    {
+        yield 'Admin user should see all broken links' =>
+        [
+            // backendUser: 1=admin
+            $this->beusers['admin'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // count
+            4
+        ];
+
+        yield 'User with no group should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // count
+            0
+        ];
+        yield 'User with permission to pages but not to specific tables should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_2.xml',
+            //pids
+            [1],
+            // count
+            0
+        ];
+        yield 'User with permission to pages and to specific tables, but no exclude fields should see 3 of 4 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 1'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_3.xml',
+            //pids
+            [1],
+            // count
+            3
+        ];
+        yield 'User with permission to pages, specific tables and exclude fields should see all broken links' =>
+        [
+            // backend user
+            $this->beusers['group 2'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_4.xml',
+            //pids
+            [1],
+            // count
+            4
+        ];
+        yield 'User has write permission only for Ctype textmedia and text, should see only broken links from textmedia records' =>
+        [
+            // backend user
+            $this->beusers['group 6'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_6_explicit_allow.xml',
+            //pids
+            [1],
+            // count
+            1
+        ];
+
+        yield 'User has write permission only for default language and should see only 1 of 2 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 3'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_5.xml',
+            //pids
+            [1],
+            // count
+            1
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider getAllBrokenLinksForPagesReturnsCorrectCountForUserDataProvider
+     */
+    public function getAllBrokenLinksForPagesReturnsCorrectCountForUser(
+        array $beuser,
+        string $inputFile,
+        array $pidList,
+        int $expectedCount
+    ) {
+        $config = [
+            'db' => '1',
+            'file' => '1',
+            'external' => '1',
+            'linkhandler' => '1'
+
+        ];
+
+        $linkTypes = [
+            'db',
+            'file',
+            'external'
+        ];
+
+        $tsConfig = [
+            'searchFields.' => [
+                'pages' => 'media,url,canonical_link',
+                'tt_content' => 'bodytext,header_link,records'
+            ],
+            'linktypes' => 'db,file,external,linkhandler',
+            'checkhidden' => '0',
+            'linkhandler' => [
+                'reportHiddenRecords' => '0'
+            ]
+        ];
+
+        $searchFields = $tsConfig['searchFields.'];
+        foreach ($searchFields as $table => $fields) {
+            $searchFields[$table] = explode(',', $fields);
+        }
+
+        $this->setupBackendUser($beuser['uid'], $beuser['fixture'], $beuser['groupFixture']);
+
+        $this->importDataSet($inputFile);
+
+        $linkAnalyzer = new LinkAnalyzer(
+            $this->prophesize(EventDispatcherInterface::class)->reveal(),
+            $this->brokenLinksRepository
+        );
+        $linkAnalyzer->init($searchFields, implode(',', $pidList), $tsConfig);
+        $linkAnalyzer->getLinkStatistics($config);
+
+        $results = $this->brokenLinksRepository->getAllBrokenLinksForPages(
+            $pidList,
+            $linkTypes,
+            $searchFields
+        );
+
+        self::assertEquals($expectedCount, count($results));
+    }
+
+    public function getAllBrokenLinksForPagesReturnsCorrectValuesForUserDataProvider()
+    {
+        yield 'Admin user should see all broken links' =>
+        [
+            // backendUser: 1=admin
+            $this->beusers['admin'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                [
+                   'record_uid' => 1,
+                   'record_pid' => 1,
+                   'language' => 0,
+                   'headline' => 'link',
+                   'field' => 'bodytext',
+                   'table_name' => 'tt_content',
+                   'element_type' => 'textmedia',
+                   'link_title' => 'link',
+                   'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                   'link_type' => 'external',
+                   'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 2,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => '[No title]',
+                    'field' => 'header_link',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => null,
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 3,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => '85',
+                    'link_type' => 'db',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 5,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => 'file:88',
+                    'link_type' => 'file',
+                    'needs_recheck' => 0
+                ],
+            ]
+        ];
+
+        yield 'User with no group should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input.xml',
+            //pids
+            [1],
+            // expected result:
+            []
+        ];
+        yield 'User with permission to pages but not to specific tables should see none' =>
+        [
+            // backend user
+            $this->beusers['no group'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_2.xml',
+            //pids
+            [1],
+            // expected result:
+            []
+        ];
+        yield 'User with permission to pages and to specific tables, but no exclude fields should see 3 of 4 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 1'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_3.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                [
+                    'record_uid' => 1,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'link',
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 3,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => '85',
+                    'link_type' => 'db',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 5,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => 'file:88',
+                    'link_type' => 'file',
+                    'needs_recheck' => 0
+                ],
+            ]
+        ];
+        yield 'User with permission to pages, specific tables and exclude fields should see all broken links' =>
+        [
+            // backend user
+            $this->beusers['group 2'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_4.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                [
+                    'record_uid' => 1,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'link',
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 2,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => '[No title]',
+                    'field' => 'header_link',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => null,
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 3,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => '85',
+                    'link_type' => 'db',
+                    'needs_recheck' => 0
+                ],
+                [
+                    'record_uid' => 5,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'broken link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'broken link',
+                    'url' => 'file:88',
+                    'link_type' => 'file',
+                    'needs_recheck' => 0
+                ],
+            ]
+        ];
+        yield 'User has write permission only for Ctype textmedia and text, should see only broken links from textmedia records' =>
+        [
+            // backend user
+            $this->beusers['group 6'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_6_explicit_allow.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                [
+                    'record_uid' => 1,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'link',
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+            ]
+        ];
+
+        yield 'User has write permission only for default language and should see only 1 of 2 broken links' =>
+        [
+            // backend user
+            $this->beusers['group 3'],
+            // input file for DB
+            __DIR__ . '/Fixtures/input_permissions_user_5.xml',
+            //pids
+            [1],
+            // expected result:
+            [
+                [
+                    'record_uid' => 1,
+                    'record_pid' => 1,
+                    'language' => 0,
+                    'headline' => 'link',
+                    'field' => 'bodytext',
+                    'table_name' => 'tt_content',
+                    'element_type' => 'textmedia',
+                    'link_title' => 'link',
+                    'url' => 'https://sfsfsfsfdfsfsdfsf/sfdsfsds',
+                    'link_type' => 'external',
+                    'needs_recheck' => 0
+                ],
+            ]
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider getAllBrokenLinksForPagesReturnsCorrectValuesForUserDataProvider
+     */
+    public function getAllBrokenLinksForPagesReturnsCorrectValuesForUser(
+        array $beuser,
+        string $inputFile,
+        array $pidList,
+        array $expectedResult
+    ) {
+        $config = [
+            'db' => '1',
+            'file' => '1',
+            'external' => '1',
+            'linkhandler' => '1'
+
+        ];
+
+        $linkTypes = [
+            'db',
+            'file',
+            'external'
+        ];
+
+        $tsConfig = [
+            'searchFields.' => [
+                'pages' => 'media,url,canonical_link',
+                'tt_content' => 'bodytext,header_link,records'
+            ],
+            'linktypes' => 'db,file,external,linkhandler',
+            'checkhidden' => '0',
+            'linkhandler' => [
+                'reportHiddenRecords' => '0'
+            ]
+        ];
+
+        $searchFields = $tsConfig['searchFields.'];
+        foreach ($searchFields as $table => $fields) {
+            $searchFields[$table] = explode(',', $fields);
+        }
+
+        $this->setupBackendUser($beuser['uid'], $beuser['fixture'], $beuser['groupFixture']);
+
+        $this->importDataSet($inputFile);
+
+        $linkAnalyzer = new LinkAnalyzer(
+            $this->prophesize(EventDispatcherInterface::class)->reveal(),
+            $this->brokenLinksRepository
+        );
+        $linkAnalyzer->init($searchFields, implode(',', $pidList), $tsConfig);
+        $linkAnalyzer->getLinkStatistics($config);
+
+        $results = $this->brokenLinksRepository->getAllBrokenLinksForPages(
+            $pidList,
+            $linkTypes,
+            $searchFields
+        );
+
+        foreach ($results as &$result) {
+            unset($result['url_response']);
+            unset($result['uid']);
+            unset($result['last_check']);
+        }
+        self::assertEquals($expectedResult, $results);
+    }
+
+    protected function setupBackendUser(int $uid, string $fixtureFile, string $groupFixtureFile)
+    {
+        if ($groupFixtureFile) {
+            $this->importDataSet($groupFixtureFile);
+        }
+        $this->backendUserFixture = $fixtureFile;
+        $this->setUpBackendUserFromFixture($uid);
+    }
+}
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml
new file mode 100644
index 000000000000..511c79f6e21a
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_groups.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <be_groups>
+        <uid>1</uid>
+        <pid>0</pid>
+        <tstamp>1366642540</tstamp>
+        <tables_modify>pages,tt_content</tables_modify>
+        <explicit_allowdeny>tt_content:CType:text:ALLOW,tt_content:CType:textmedia:ALLOW</explicit_allowdeny>
+    </be_groups>
+    <be_groups>
+        <uid>2</uid>
+        <pid>0</pid>
+        <tstamp>1366642540</tstamp>
+        <tables_modify>pages,tt_content</tables_modify>
+        <non_exclude_fields>tt_content:header_link</non_exclude_fields>
+        <explicit_allowdeny>tt_content:CType:text:ALLOW,tt_content:CType:textmedia:ALLOW</explicit_allowdeny>
+    </be_groups>
+    <be_groups>
+        <uid>3</uid>
+        <pid>0</pid>
+        <tstamp>1366642540</tstamp>
+        <tables_modify>pages,tt_content</tables_modify>
+        <allowed_languages>0</allowed_languages>
+        <explicit_allowdeny>tt_content:CType:text:ALLOW,tt_content:CType:textmedia:ALLOW</explicit_allowdeny>
+    </be_groups>
+
+    <!-- group 6: editors with access to all, but only CType=textmedia and text via explicit allow -->
+    <be_groups>
+        <uid>6</uid>
+        <pid>0</pid>
+        <tstamp>1366642540</tstamp>
+        <tables_modify>pages,tt_content</tables_modify>
+        <explicit_allowdeny>tt_content:CType:text:ALLOW,tt_content:CType:textmedia:ALLOW</explicit_allowdeny>
+    </be_groups>
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml
new file mode 100644
index 000000000000..e8df1d0f7e7f
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/be_users.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+	<be_users>
+		<uid>2</uid>
+		<pid>0</pid>
+		<username>plain_editor</username>
+        <realName>Editor with no group</realName>
+		<password>$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1</password> <!-- password -->
+		<admin>0</admin>
+		<disableIPlock>1</disableIPlock>
+		<lastlogin>1371033743</lastlogin>
+		<createdByAction>0</createdByAction>
+		<workspace_id>0</workspace_id>
+	</be_users>
+
+    <!-- editor with access to tables via group -->
+    <be_users>
+        <uid>3</uid>
+        <pid>0</pid>
+        <username>simple_editor1</username>
+        <realName>Editor with group 1</realName>
+        <usergroup>1</usergroup>
+        <password>$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1</password> <!-- password -->
+        <admin>0</admin>
+        <disableIPlock>1</disableIPlock>
+        <lastlogin>1371033743</lastlogin>
+        <createdByAction>0</createdByAction>
+        <workspace_id>0</workspace_id>
+    </be_users>
+
+    <!-- editor with access to tables and excludefiels via group -->
+    <be_users>
+        <uid>4</uid>
+        <pid>0</pid>
+        <username>simple_editor2</username>
+        <realName>Editor with group 2</realName>
+        <usergroup>2</usergroup>
+        <password>$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1</password> <!-- password -->
+        <admin>0</admin>
+        <disableIPlock>1</disableIPlock>
+        <lastlogin>1371033743</lastlogin>
+        <createdByAction>0</createdByAction>
+        <workspace_id>0</workspace_id>
+    </be_users>
+
+    <!-- editor with access to tables, limited to default language -->
+    <be_users>
+        <uid>5</uid>
+        <pid>0</pid>
+        <username>simple_editor3</username>
+        <realName>Editor with group 3</realName>
+        <usergroup>3</usergroup>
+        <password>$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1</password> <!-- password -->
+        <admin>0</admin>
+        <disableIPlock>1</disableIPlock>
+        <lastlogin>1371033743</lastlogin>
+        <createdByAction>0</createdByAction>
+        <workspace_id>0</workspace_id>
+    </be_users>
+
+    <!-- user 6: editor with access to all, but only CType=textmedia and text via explicit allow -->
+    <be_users>
+        <uid>6</uid>
+        <pid>0</pid>
+        <username>simple_editor6</username>
+        <realName>Editor with group 6</realName>
+        <usergroup>6</usergroup>
+        <password>$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1</password> <!-- password -->
+        <admin>0</admin>
+        <disableIPlock>1</disableIPlock>
+        <lastlogin>1371033743</lastlogin>
+        <createdByAction>0</createdByAction>
+        <workspace_id>0</workspace_id>
+    </be_users>
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input.xml
new file mode 100644
index 000000000000..7dd9c7e3dc44
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+    </pages>
+    <!-- external -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+    <tt_content>
+        <uid>2</uid>
+        <pid>1</pid>
+        <header_link>https://sfsfsfsfdfsfsdfsf/sfdsfsds</header_link>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- page -->
+    <tt_content>
+        <uid>3</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://page?uid=85&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- file -->
+    <tt_content>
+        <uid>5</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://file?uid=88&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_group.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_group.xml
new file mode 100644
index 000000000000..d29a39db86a3
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_group.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_group>1</perms_group>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- external -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- page -->
+    <tt_content>
+        <uid>3</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://page?uid=85&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- file -->
+    <tt_content>
+        <uid>5</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://file?uid=88&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_2.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_2.xml
new file mode 100644
index 000000000000..c45d3aad8172
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_2.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_userid>2</perms_userid>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- external -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- page -->
+    <tt_content>
+        <uid>3</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://page?uid=85&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- file -->
+    <tt_content>
+        <uid>5</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://file?uid=88&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_3.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_3.xml
new file mode 100644
index 000000000000..b9eb96931d2d
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_3.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_userid>3</perms_userid>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- external -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+    <tt_content>
+        <uid>2</uid>
+        <pid>1</pid>
+        <header_link>https://sfsfsfsfdfsfsdfsf/sfdsfsds</header_link>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- page -->
+    <tt_content>
+        <uid>3</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://page?uid=85&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- file -->
+    <tt_content>
+        <uid>5</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://file?uid=88&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_4.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_4.xml
new file mode 100644
index 000000000000..9aebe00c5636
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_4.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_userid>4</perms_userid>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- external -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+    <tt_content>
+        <uid>2</uid>
+        <pid>1</pid>
+        <header_link>https://sfsfsfsfdfsfsdfsf/sfdsfsds</header_link>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- page -->
+    <tt_content>
+        <uid>3</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://page?uid=85&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- file -->
+    <tt_content>
+        <uid>5</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;t3://file?uid=88&quot;&gt;broken link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_5.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_5.xml
new file mode 100644
index 000000000000..e69790676137
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_5.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_userid>5</perms_userid>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- default language -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+        <sys_language_uid>0</sys_language_uid>
+    </tt_content>
+
+    <!-- other language -->
+    <tt_content>
+        <uid>2</uid>
+        <pid>1</pid>
+        <header_link>https://sfsfsfsfdfsfsdfsf/sfdsfsds</header_link>
+        <CType>textmedia</CType>
+        <sys_language_uid>1</sys_language_uid>
+    </tt_content>
+
+</dataset>
diff --git a/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_6_explicit_allow.xml b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_6_explicit_allow.xml
new file mode 100644
index 000000000000..7624e10cdb3c
--- /dev/null
+++ b/typo3/sysext/linkvalidator/Tests/Functional/Repository/Fixtures/input_permissions_user_6_explicit_allow.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <perms_userid>6</perms_userid>
+        <!-- Permission::CONTENT_EDIT | Permission::PAGE_EDIT -->
+        <perms_user>18</perms_user>
+    </pages>
+
+    <!-- textmedia -->
+    <tt_content>
+        <uid>1</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textmedia</CType>
+    </tt_content>
+
+    <!-- textpic -->
+    <tt_content>
+        <uid>2</uid>
+        <pid>1</pid>
+        <bodytext>&lt;p&gt;&lt;a href=&quot;https://sfsfsfsfdfsfsdfsf/sfdsfsds&quot;&gt;link&lt;/a&gt;&lt;/p&gt;</bodytext>
+        <CType>textpic</CType>
+    </tt_content>
+</dataset>
diff --git a/typo3/sysext/linkvalidator/ext_tables.sql b/typo3/sysext/linkvalidator/ext_tables.sql
index 401cc85e9c30..91b4b19d6564 100644
--- a/typo3/sysext/linkvalidator/ext_tables.sql
+++ b/typo3/sysext/linkvalidator/ext_tables.sql
@@ -2,9 +2,11 @@ CREATE TABLE tx_linkvalidator_link (
 	uid int(11) NOT NULL auto_increment,
 	record_uid int(11) DEFAULT '0' NOT NULL,
 	record_pid int(11) DEFAULT '0' NOT NULL,
+	language int(11) DEFAULT '-1' NOT NULL,
 	headline varchar(255) DEFAULT '' NOT NULL,
 	field varchar(255) DEFAULT '' NOT NULL,
 	table_name varchar(255) DEFAULT '' NOT NULL,
+	element_type varchar(255) DEFAULT '' NOT NULL,
 	link_title text,
 	url text,
 	url_response text,
-- 
GitLab