Skip to content
Snippets Groups Projects
Commit 35543830 authored by Pavlo Nagornyak's avatar Pavlo Nagornyak Committed by Alexander Hass
Browse files

Issue #2992604 by pnagornyak, waverate: Broken links report

parent 5b809cd2
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
...@@ -3,13 +3,3 @@ linkchecker.admin_settings_form: ...@@ -3,13 +3,3 @@ linkchecker.admin_settings_form:
parent: system.admin_config_content parent: system.admin_config_content
description: 'Configure the content types that should be checked for broken links and how the hypertext links will be checked and reported and repaired.' description: 'Configure the content types that should be checked for broken links and how the hypertext links will be checked and reported and repaired.'
route_name: linkchecker.admin_settings_form route_name: linkchecker.admin_settings_form
linkchecker.admin_report_page:
title: 'Broken links'
parent: system.admin_reports
description: 'Shows a list of broken links in content.'
route_name: linkchecker.admin_report_page
linkchecker.user_report_page:
title: 'Broken links'
parent: user.page
description: 'Shows a list of broken links in content.'
route_name: linkchecker.user_report_page
...@@ -2,11 +2,3 @@ linkchecker.admin_settings_form_tab: ...@@ -2,11 +2,3 @@ linkchecker.admin_settings_form_tab:
route_name: linkchecker.admin_settings_form route_name: linkchecker.admin_settings_form
title: Settings title: Settings
base_route: linkchecker.admin_settings_form base_route: linkchecker.admin_settings_form
linkchecker.admin_report_page_tab:
route_name: linkchecker.admin_report_page
title: Broken links
base_route: linkchecker.admin_report_page
linkchecker.user_report_page_tab:
route_name: linkchecker.user_report_page
title: Broken links
base_route: linkchecker.user_report_page
<?php
/**
* @file
* User page callbacks for the linkchecker module.
*/
use Drupal\Component\Utility\Html;
/**
* Menu callback for general reporting.
*
* @return string
* Themed report page.
*/
function linkchecker_admin_report_page() {
$ignore_response_codes = preg_split('/(\r\n?|\n)/', \Drupal::config('linkchecker.settings')->get('linkchecker_ignore_response_codes'));
// Search for broken links in nodes and comments and blocks of all users.
// @todo Try to make UNION'ed subselect resultset smaller.
$subquery4 = db_select('linkchecker_node', 'ln')
->distinct()
->fields('ln', ['lid']);
$subquery3 = db_select('linkchecker_comment', 'lc')
->distinct()
->fields('lc', ['lid']);
$subquery2 = db_select('linkchecker_block_custom', 'lb')
->distinct()
->fields('lb', ['lid']);
// UNION all linkchecker type tables.
$subquery1 = db_select($subquery2->union($subquery3)->union($subquery4), 'q1')->fields('q1', ['lid']);
// Build pager query.
$query = db_select('linkchecker_link', 'll')->extend('PagerDefault')->extend('TableSort');
$query->innerJoin($subquery1, 'q2', 'q2.lid = ll.lid');
$query->fields('ll');
$query->condition('ll.last_checked', 0, '<>');
$query->condition('ll.status', 1);
$query->condition('ll.code', $ignore_response_codes, 'NOT IN');
return _linkchecker_report_page($query);
}
/**
* Menu callback for author specific reporting.
*
* @param object $account
* The user account.
*
* @return string
* Themed report page.
*/
function linkchecker_user_report_page($account) {
drupal_set_title($account->name);
$ignore_response_codes = preg_split('/(\r\n?|\n)/', \Drupal::config('linkchecker.settings')->get('linkchecker_ignore_response_codes'));
// Build query for broken links in nodes of the current user.
$subquery2 = db_select('node', 'n');
$subquery2->innerJoin('node_revision', 'r', 'r.vid = n.vid');
$subquery2->innerJoin('linkchecker_node', 'ln', 'ln.nid = n.nid');
$subquery2->innerJoin('linkchecker_link', 'll', 'll.lid = ln.lid');
$subquery2->condition('ll.last_checked', 0, '<>');
$subquery2->condition('ll.status', 1);
$subquery2->condition('ll.code', $ignore_response_codes, 'NOT IN');
$subquery2->condition(db_or()
->condition('n.uid', $account->uid)
->condition('r.uid', $account->uid)
);
$subquery2->distinct();
$subquery2->fields('ll', ['lid']);
$comment_types = linkchecker_scan_comment_types();
if (!empty($comment_types)) {
// Build query for broken links in nodes and comments of the current user.
$subquery3 = db_select('comment', 'c');
$subquery3->innerJoin('linkchecker_comment', 'lc', 'lc.cid = c.cid');
$subquery3->innerJoin('linkchecker_link', 'll', 'll.lid = lc.lid');
$subquery3->condition('ll.last_checked', 0, '<>');
$subquery3->condition('ll.status', 1);
$subquery3->condition('ll.code', $ignore_response_codes, 'NOT IN');
$subquery3->condition('c.uid', $account->uid);
$subquery3->distinct();
$subquery3->fields('ll', ['lid']);
// UNION the linkchecker_node and linkchecker_comment tables.
$subquery1 = db_select($subquery2->union($subquery3), 'q1')->fields('q1', ['lid']);
}
else {
// Build query for broken links in nodes of the current user.
$subquery1 = db_select($subquery2, 'q1')->fields('q1', ['lid']);
}
// Build pager query.
$query = db_select('linkchecker_link', 'll')->extend('PagerDefault')->extend('TableSort');
$query->innerJoin($subquery1, 'q2', 'q2.lid = ll.lid');
$query->fields('ll');
$query->condition('ll.last_checked', 0, '<>');
$query->condition('ll.status', 1);
$query->condition('ll.code', $ignore_response_codes, 'NOT IN');
return _linkchecker_report_page($query, $account);
}
/**
* Builds the HTML report page table with pager.
*
* @param SelectQueryInterface $query
* The pager query for the report page. Can be per user report or global.
* @param object|null $account
* The user account object.
*
* @return string
* Themed report page.
*/
function _linkchecker_report_page($query, $account = NULL) {
$links_unchecked = db_query('SELECT COUNT(1) FROM {linkchecker_link} WHERE last_checked = :last_checked AND status = :status', [':last_checked' => 0, ':status' => 1])->fetchField();
if ($links_unchecked > 0) {
$links_all = db_query('SELECT COUNT(1) FROM {linkchecker_link} WHERE status = :status', [':status' => 1])->fetchField();
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($links_unchecked,
'There is 1 unchecked link of about @links_all links in the database. Please be patient until all links have been checked via cron.',
'There are @count unchecked links of about @links_all links in the database. Please be patient until all links have been checked via cron.',
['@links_all' => $links_all]), 'warning');
}
$header = [
['data' => t('URL'), 'field' => 'url', 'sort' => 'desc'],
['data' => t('Response'), 'field' => 'code', 'sort' => 'desc'],
['data' => t('Error'), 'field' => 'error'],
['data' => t('Operations')],
];
$result = $query
->limit(50)
->orderByHeader($header)
->execute();
// Evaluate permission once for performance reasons.
$access_edit_link_settings = \Drupal::currentUser()->hasPermission('edit linkchecker link settings');
$access_administer_blocks = \Drupal::currentUser()->hasPermission('administer blocks');
$access_administer_redirects = \Drupal::currentUser()->hasPermission('administer redirects');
$rows = [];
foreach ($result as $link) {
// Get the node, block and comment IDs that refer to this broken link and
// that the current user has access to.
$nids = _linkchecker_link_node_ids($link, $account);
$cids = _linkchecker_link_comment_ids($link, $account);
$bids = _linkchecker_link_block_ids($link);
// If the user does not have access to see this link anywhere, do not
// display it, for reasons explained in _linkchecker_link_access(). We
// still need to fill the table row, though, so as not to throw off the
// number of items in the pager.
if (empty($nids) && empty($cids) && empty($bids)) {
$rows[] = [['data' => t('Permission restrictions deny you access to this broken link.'), 'colspan' => count($header)]];
continue;
}
$links = [];
// Show links to link settings.
if ($access_edit_link_settings) {
$links[] = l(t('Edit link settings'), 'linkchecker/' . $link->lid . '/edit', ['query' => drupal_get_destination()]);
}
// Show link to nodes having this broken link.
foreach ($nids as $nid) {
$links[] = l(t('Edit node @node', ['@node' => $nid]), 'node/' . $nid . '/edit', ['query' => drupal_get_destination()]);
}
// Show link to comments having this broken link.
$comment_types = linkchecker_scan_comment_types();
if (\Drupal::moduleHandler()->moduleExists('comment') && !empty($comment_types)) {
foreach ($cids as $cid) {
$links[] = l(t('Edit comment @comment', ['@comment' => $cid]), 'comment/' . $cid . '/edit', ['query' => drupal_get_destination()]);
}
}
// Show link to blocks having this broken link.
if ($access_administer_blocks) {
foreach ($bids as $bid) {
$links[] = l(t('Edit block @block', ['@block' => $bid]), 'admin/structure/block/manage/block/' . $bid . '/configure', ['query' => drupal_get_destination()]);
}
}
// Show link to redirect this broken internal link.
if (\Drupal::moduleHandler()->moduleExists('redirect') && $access_administer_redirects && _linkchecker_is_internal_url($link)) {
$links[] = l(t('Create redirection'), 'admin/config/search/redirect/add', ['query' => ['source' => $link->internal, drupal_get_destination()]]);
}
// Create table data for output.
$rows[] = [
'data' => [
l(_filter_url_trim($link->url, 40), $link->url),
$link->code,
Html::escape($link->error),
theme('item_list', ['items' => $links]),
],
];
}
$build['linkchecker_table'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No broken links have been found.'),
];
$build['linkchecker_pager'] = ['#theme' => 'pager'];
return $build;
}
/**
* Check if the link is an internal URL or not.
*
* @param object $link
* Link object.
*
* @return bool
* TRUE if link is internal, otherwise FALSE.
*/
function _linkchecker_is_internal_url(&$link) {
global $base_url;
if (strpos($link->url, $base_url) === 0) {
$link->internal = trim(substr($link->url, strlen($base_url)), " \t\r\n\0\\/");
return TRUE;
}
}
...@@ -5,22 +5,3 @@ linkchecker.admin_settings_form: ...@@ -5,22 +5,3 @@ linkchecker.admin_settings_form:
_title: 'Link checker' _title: 'Link checker'
requirements: requirements:
_permission: 'administer linkchecker' _permission: 'administer linkchecker'
linkchecker.admin_report_page:
path: '/admin/reports/linkchecker'
defaults:
_controller: '\Drupal\linkchecker\Controller\LinkCheckerAdminReportPage::content'
_title: 'Broken links'
requirements:
_permission: 'access broken links report'
linkchecker.user_report_page:
path: '/user/{user}/linkchecker'
defaults:
_title: 'Broken links'
_controller: '\Drupal\linkchecker\Controller\LinkCheckerUserReportPage::content'
options:
_admin_route: TRUE
requirements:
_custom_access: '\Drupal\linkchecker\Controller\LinkCheckerUserReportPage::access'
user: \d+
<?php
namespace Drupal\linkchecker\Controller;
use Drupal\Core\Url;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Controller\ControllerBase;
/**
* Builds admin broken link report page.
*/
class LinkCheckerAdminReportPage extends ControllerBase {
public function content() {
$ignore_response_codes = preg_split('/(\r\n?|\n)/', \Drupal::config('linkchecker.settings')->get('error.ignore_response_codes'));
// Search for broken links in nodes and comments and blocks of all users.
// @todo Try to make UNION'ed subselect resultset smaller.
$subquery4 = \Drupal::database()->select('linkchecker_node', 'ln')
->distinct()
->fields('ln', ['lid']);
$subquery3 = \Drupal::database()->select('linkchecker_comment', 'lc')
->distinct()
->fields('lc', ['lid']);
$subquery2 = \Drupal::database()->select('linkchecker_block_custom', 'lb')
->distinct()
->fields('lb', ['lid']);
// UNION all linkchecker type tables.
$subquery1 = \Drupal::database()->select($subquery2->union($subquery3)->union($subquery4), 'q1')->fields('q1', ['lid']);
// Build pager query.
$query = \Drupal::database()->select('linkchecker_link', 'll');
$query->innerJoin($subquery1, 'q2', 'q2.lid = ll.lid');
$query->fields('ll');
$query->condition('ll.last_checked', 0, '<>');
$query->condition('ll.status', 1);
$query->condition('ll.code', $ignore_response_codes, 'NOT IN');
$query->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->extend('Drupal\Core\Database\Query\TableSortExtender');
return $this->_linkchecker_report_page($query);
}
/**
* Builds the HTML report page table with pager.
*
* @param SelectQueryInterface $query
* The pager query for the report page. Can be per user report or global.
* @param object|null $account
* The user account object.
*
* @return string
* Themed report page.
*/
public function _linkchecker_report_page($query, $account = NULL) {
$connection = \Drupal::database();
$links_unchecked = $connection->query('SELECT COUNT(1) FROM {linkchecker_link} WHERE last_checked = :last_checked AND status = :status', [':last_checked' => 0, ':status' => 1])->fetchField();
if ($links_unchecked > 0) {
$links_all = $connection->query('SELECT COUNT(1) FROM {linkchecker_link} WHERE status = :status', [':status' => 1])->fetchField();
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($links_unchecked,
'There is 1 unchecked link of about @links_all links in the database. Please be patient until all links have been checked via cron.',
'There are @count unchecked links of about @links_all links in the database. Please be patient until all links have been checked via cron.',
['@links_all' => $links_all]
),
'warning'
);
}
$header = [
['data' => t('URL'), 'field' => 'url', 'sort' => 'desc'],
['data' => t('Response'), 'field' => 'code', 'sort' => 'desc'],
['data' => t('Error'), 'field' => 'error'],
['data' => t('Operations')],
];
$result = $connection->query('SELECT * FROM {linkchecker_link}')
->fetchAll();
// Evaluate permission once for performance reasons.
$account = \Drupal::currentUser();
$access_edit_link_settings = $account->hasPermission('edit linkchecker link settings');
$access_administer_blocks = $account->hasPermission('administer blocks');
$access_administer_redirects = $account->hasPermission('administer redirects');
$rows = [];
foreach ($result as $link) {
// Get the node, block and comment IDs that refer to this broken link and
// that the current user has access to.
$cids = _linkchecker_link_comment_ids($link, $account);
$bids = _linkchecker_link_block_ids($link);
$nids = $connection->query('SELECT nid FROM {linkchecker_node} WHERE lid = :lid', [':lid' => $link->lid])->fetchCol();
// If the user does not have access to see this link anywhere, do not
// display it, for reasons explained in _linkchecker_link_access(). We
// still need to fill the table row, though, so as not to throw off the
// number of items in the pager.
if (empty($nids) && empty($cids) && empty($bids)) {
$rows[] = array(array('data' => t('Permission restrictions deny you access to this broken link.'), 'colspan' => count($header)));
continue;
}
$links = array();
// Show links to link settings.
// if ($access_edit_link_settings) {
// $links[] = $this->l(t('Edit link settings'), Url::fromUri('base:' . 'linkchecker/' . $link->lid . '/edit'), array('query' => drupal_get_destination()));
//}
// Show link to nodes having this broken link.
foreach ($nids as $nid) {
$links[] = $this->l(t('Edit node @node', array('@node' => $nid)), Url::fromUri('base:' . 'node/' . $nid . '/edit'), array('query' => drupal_get_destination()));
}
// Show link to comments having this broken link.
$comment_types = linkchecker_scan_comment_types();
if (\Drupal::moduleHandler()->moduleExists('comment') && !empty($comment_types)) {
foreach ($cids as $cid) {
$links[] = $this->l(t('Edit comment @comment', array('@comment' => $cid)), Url::fromUri('base:' . 'comment/' . $cid . '/edit'), array('query' => drupal_get_destination()));
}
}
// Show link to blocks having this broken link.
if ($access_administer_blocks) {
foreach ($bids as $bid) {
$links[] = $this->l(t('Edit block @block', array('@block' => $bid)), Url::fromUri('base:' . 'admin/structure/block/manage/block/' . $bid . '/configure') , array('query' => drupal_get_destination()));
}
}
// Show link to redirect this broken internal link.
if (\Drupal::moduleHandler()->moduleExists('redirect') && $access_administer_redirects && $this->_linkchecker_is_internal_url($link)) {
$links[] = $this->l(t('Create redirection'), Url::fromUri('base:' . 'admin/config/search/redirect/add'), array('query' => array('source' => $link->internal, drupal_get_destination())));
}
// Create table data for output.
$items = array(
'#theme' => 'item_list',
'#items' => $links,
'#title' => t(''),
);
$rows[] = array(
'data' => array(
$this->l(_filter_url_trim($link->url, 60), Url::fromUri($link->url)),
$link->code,
SafeMarkup::checkPlain($link->error),
drupal_render($items),
),
);
}
$build['linkchecker_table'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No broken links have been found.'),
);
//$build['linkchecker_pager'] = ['#theme' => 'pager'];
return $build;
}
/**
* Check if the link is an internal URL or not.
*
* @param object $link
* Link object.
*
* @return bool
* TRUE if link is internal, otherwise FALSE.
*/
public function _linkchecker_is_internal_url(&$link) {
global $base_url;
if (strpos($link->url, $base_url) === 0) {
$link->internal = trim(substr($link->url, strlen($base_url)), " \t\r\n\0\\/");
return TRUE;
}
}
}
<?php
namespace Drupal\linkchecker\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Builds user broken link report page.
*/
class LinkCheckerUserReportPage {
/**
* @return string
*/
public function content() {
return '@TODO';
}
/**
* Checks access for a specific request.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
*
* @return \Drupal\Core\Access\AccessResult
*/
public function access(AccountInterface $account) {
$user = \Drupal::currentUser();
// Users with 'access own broken links report' permission can only view
// their own report. Users with the 'access broken links report' permission
// can view the report for any authenticated user.
return AccessResult::allowedIf($account->id() && (($user->id()) == $account->id()) && $account->hasPermission('access own broken links report') || $account->hasPermission('access broken links report'));
}
}
...@@ -23,30 +23,8 @@ class LinkCheckerLinkAccessControlHandler extends EntityAccessControlHandler { ...@@ -23,30 +23,8 @@ class LinkCheckerLinkAccessControlHandler extends EntityAccessControlHandler {
/** @var \Drupal\linkchecker\LinkCheckerLinkInterface $entity */ /** @var \Drupal\linkchecker\LinkCheckerLinkInterface $entity */
if ($account->hasPermission('administer linkchecker') if ($account->hasPermission('administer linkchecker')
|| $account->hasPermission('edit linkchecker link settings')) { || $account->hasPermission('edit linkchecker link settings')) {
$parentEntity = $entity->getParentEntity();
if (isset($parentEntity) && !$parentEntity->access($operation, $account)) { return $this->checkParentEntityAccess($entity, $operation, $account);
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
if (!$parentEntity->hasField($entity->getParentEntityFieldName())) {
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
$parentEntityField = $parentEntity->get($entity->getParentEntityFieldName());
if (!$parentEntityField->access($operation, $account)) {
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
return AccessResult::allowed()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
} }
// The permission is required. // The permission is required.
...@@ -72,7 +50,55 @@ class LinkCheckerLinkAccessControlHandler extends EntityAccessControlHandler { ...@@ -72,7 +50,55 @@ class LinkCheckerLinkAccessControlHandler extends EntityAccessControlHandler {
} }
} }
// User not allowed to view URL field if he does not have access to parent
// entity.
if ($operation == 'view'
&& isset($items)
&& $field_definition->getName() == 'url') {
return $this->checkParentEntityAccess($items->getEntity(), $operation, $account);
}
return parent::checkFieldAccess($operation, $field_definition, $account, $items); return parent::checkFieldAccess($operation, $field_definition, $account, $items);
} }
/**
* Helper function for access checking.
*/
protected function checkParentEntityAccess(LinkCheckerLinkInterface $entity, $operation, AccountInterface $account) {
$parentEntity = $entity->getParentEntity();
// If parent not exists - forbidden.
if (!isset($parentEntity)) {
return AccessResult::forbidden()
->cachePerPermissions();
}
// If user does not have access to parent entity - forbidden.
if (!$parentEntity->access($operation, $account)) {
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
// If field where link was stored not exists anymore - forbidden.
if (!$parentEntity->hasField($entity->getParentEntityFieldName())) {
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
// If user does not have access to field where link is stored - forbidden.
$parentEntityField = $parentEntity->get($entity->getParentEntityFieldName());
if (!$parentEntityField->access($operation, $account)) {
return AccessResult::forbidden()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
// Allowed in all other cases.
return AccessResult::allowed()
->addCacheableDependency($parentEntity)
->cachePerPermissions();
}
} }
...@@ -166,6 +166,27 @@ class LinkCheckerLinkAccessTest extends KernelTestBase { ...@@ -166,6 +166,27 @@ class LinkCheckerLinkAccessTest extends KernelTestBase {
], $link, $user); ], $link, $user);
} }
} }
// Check access if parent entity is not exists.
// By default such links will be cleaned up, but in a case if somebody will
// remove parent entity directly from database we should handle it.
$link = LinkCheckerLink::create([
'url' => 'http://example.com/',
'entity_id' => [
'target_id' => 9999,
'target_type' => 'node',
],
'entity_field' => 'test_text_field',
'entity_langcode' => 'en',
]);
$link->save();
foreach ($webUsers as $user) {
$this->assertLinkAccess([
'view' => FALSE,
], $link, $user);
}
} }
/** /**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment