Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/linkchecker
  • issue/linkchecker-3186322
  • issue/linkchecker-3186356
  • issue/linkchecker-3092304
  • issue/linkchecker-3187256
  • issue/linkchecker-3058014
  • issue/linkchecker-3182061
  • issue/linkchecker-3188928
  • issue/linkchecker-3184613
  • issue/linkchecker-3218314
  • issue/linkchecker-3218429
  • issue/linkchecker-3203329
  • issue/linkchecker-3112537
  • issue/linkchecker-3247070
  • issue/linkchecker-3248989
  • issue/linkchecker-3240788
  • issue/linkchecker-3259505
  • issue/linkchecker-3259507
  • issue/linkchecker-3265254
  • issue/linkchecker-3270309
  • issue/linkchecker-3247823
  • issue/linkchecker-3271859
  • issue/linkchecker-3271896
  • issue/linkchecker-3271897
  • issue/linkchecker-3272881
  • issue/linkchecker-3276199
  • issue/linkchecker-3272008
  • issue/linkchecker-3294846
  • issue/linkchecker-3308730
  • issue/linkchecker-3310572
  • issue/linkchecker-3244743
  • issue/linkchecker-3331499
  • issue/linkchecker-3334240
  • issue/linkchecker-3335579
  • issue/linkchecker-3336977
  • issue/linkchecker-3346793
  • issue/linkchecker-3348052
  • issue/linkchecker-3360756
  • issue/linkchecker-3366753
  • issue/linkchecker-3402569
  • issue/linkchecker-3375253
  • issue/linkchecker-3376854
  • issue/linkchecker-3313343
  • issue/linkchecker-3377179
  • issue/linkchecker-3389739
  • issue/linkchecker-3375553
  • issue/linkchecker-3396924
  • issue/linkchecker-3406749
  • issue/linkchecker-3412596
  • issue/linkchecker-3335586
  • issue/linkchecker-3065056
  • issue/linkchecker-3418170
  • issue/linkchecker-3422365
  • issue/linkchecker-3426637
  • issue/linkchecker-3386764
  • issue/linkchecker-3438441
  • issue/linkchecker-3441943
  • issue/linkchecker-3405194
  • issue/linkchecker-3247797
  • issue/linkchecker-2958196
  • issue/linkchecker-3432936
  • issue/linkchecker-3437481
  • issue/linkchecker-3455248
  • issue/linkchecker-3465025
  • issue/linkchecker-3334357
  • issue/linkchecker-3469979
  • issue/linkchecker-3471862
  • issue/linkchecker-3365483
  • issue/linkchecker-3489210
  • issue/linkchecker-3491838
  • issue/linkchecker-3213210
  • issue/linkchecker-3497051
  • issue/linkchecker-3497057
  • issue/linkchecker-2403161
  • issue/linkchecker-3500308
  • issue/linkchecker-3468342
  • issue/linkchecker-3468352
  • issue/linkchecker-3502665
  • issue/linkchecker-3482160
  • issue/linkchecker-3334610
  • issue/linkchecker-3369063
  • issue/linkchecker-3500383
82 results
Show changes
Commits on Source (10)
Showing
with 432 additions and 141 deletions
aliquyam
Carsten
CLSID
clsid
Cortado
Eirik
eirmod
flashvars
fluendo
flvplayer
Hass
invidunt
lcch
lccl
linkcheckerlink
Logemann
Morland
Nihonium
Nodegard
nonumy
Oganesson
pluginurl
quicktime
ritmo
Roentgenium
speex
Tennessine
urlhash
weblinks
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
variables:
OPT_IN_TEST_NEXT_MINOR: 1
OPT_IN_TEST_NEXT_MAJOR: 1
RUN_JOB_UPGRADE_STATUS: 1
cspell:
allow_failure: false
phpcs:
allow_failure: false
phpstan:
allow_failure: false
phpstan (next minor):
allow_failure: false
phpstan (next major):
allow_failure: false
phpunit (next minor):
allow_failure: false
phpunit (next major):
allow_failure: false
upgrade status:
allow_failure: false
......@@ -29,10 +29,6 @@ Administration > Reports > Broken links.
REQUIREMENTS
------------
This module requires the following module:
* Dynamic Entity Reference (https://www.drupal.org/project/dynamic_entity_reference)
For internal URL extraction you need to make sure that Cron always get called
with your real public site URL (for e.g. https://example.com/cron.php). Make
sure it's never executed with https://localhost/cron.php or any other
......
......@@ -19,8 +19,7 @@
},
"license": "GPL-2.0-or-later",
"require": {
"drupal/core": "^9.4 || ^10.0",
"drupal/dynamic_entity_reference": "^1.16 || ^2.0 || ^3.0 || ^4.0"
"drupal/core": "^9.4 || ^10.0 || ^11"
},
"require-dev": {
"drupal/redirect": "^1.8"
......
......@@ -16,6 +16,9 @@ linkchecker.settings:
base_path:
type: string
label: 'Base path of internal URLs'
search_published_contents_only:
type: boolean
label: 'Search published contents only'
extract:
type: mapping
label: 'Link extraction'
......
build:
environment:
startcontainers:
runcontainers:
create_db:
dbcreate:
codebase:
assemble_codebase:
checkout_core:
checkout.contrib:
fetch:
patch:
composer.core_install:
gather_dependencies:
update_build:
yarn_install:
start_phantomjs:
assessment:
validate_codebase:
phplint:
container_composer:
csslint:
eslint:
phpcs:
testing:
run_tests.standard:
types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
suppress-deprecations: false
run_tests.js:
concurrency: 1
types: 'PHPUnit-FunctionalJavascript'
suppress-deprecations: false
nightwatchjs:
......@@ -4,9 +4,6 @@
*/
(function ($, Drupal) {
'use strict';
/**
* Behaviors for setting summaries on content type form.
*
......@@ -14,23 +11,27 @@
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behaviors on content type edit forms.
*/
*/
Drupal.behaviors.linkcheckerContentTypes = {
attach: function (context) {
var $context = $(context);
attach(context) {
const $context = $(context);
// Provide the vertical tab summaries.
$context.find('#edit-linkchecker').drupalSetSummary(function (context) {
var vals = [];
var $editContext = $(context);
$editContext.find('input:checked').next('label').each(function () {
vals.push(Drupal.checkPlain($(this).text()));
$context
.find('#edit-linkchecker')
.drupalSetSummary(function (editContext) {
const values = [];
const $editContext = $(editContext);
$editContext
.find('input:checked')
.next('label')
.each(function () {
values.push(Drupal.checkPlain($(this).textContent));
});
if (!values.length) {
return Drupal.t('Disabled');
}
return values.join(', ');
});
if (!vals.length) {
return Drupal.t('Disabled');
}
return vals.join(', ');
});
}
},
};
})(jQuery, Drupal);
name: 'Link checker'
type: module
description: 'Periodically checks for broken links in content.'
core_version_requirement: ^9.4 || ^10.0
core_version_requirement: ^9.4 || ^10.0 || ^11
configure: linkchecker.admin_settings_form
dependencies:
- dynamic_entity_reference:dynamic_entity_reference
test_dependencies:
- redirect:redirect
......@@ -8,6 +8,8 @@
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\User;
/**
......@@ -69,3 +71,147 @@ function linkchecker_update_8002() {
->condition('last_check', 0)
->execute();
}
/**
* Add a field for linkcheckerlink uuid.
*/
function linkchecker_update_8003(): void {
\Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition(
'uuid',
'linkcheckerlink',
'linkchecker',
BaseFieldDefinition::create('uuid')
->setLabel(new TranslatableMarkup('UUID'))
->setDescription(new TranslatableMarkup('The entity UUID.'))
->setReadOnly(TRUE)
);
}
/**
* Generate uuids for new uuid field.
*/
function linkchecker_update_8004(&$sandbox) {
$storage_handler = \Drupal::entityTypeManager()->getStorage('linkcheckerlink');
$uuid_service = \Drupal::service('uuid');
$items_per_batch = 10;
if (!isset($sandbox['progress'])) {
$sandbox['progress'] = 0;
// All the entities should be updated.
$sandbox['max'] = $storage_handler
->getQuery()
->accessCheck(FALSE)
->notExists('uuid')
->count()
->execute();
}
$ids = $storage_handler
->getQuery()
->accessCheck(FALSE)
->notExists('uuid')
->range(0, $items_per_batch)
->sort('lid')
->execute();
if (!empty($ids)) {
foreach ($storage_handler->loadMultiple($ids) as $linkcheckerlink) {
$linkcheckerlink->set('uuid', $uuid_service->generate());
$linkcheckerlink->save();
$sandbox['progress']++;
}
}
$sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']);
if ($sandbox['#finished']) {
return t('Entities are updating: finished @progress of @total.', [
'@progress' => $sandbox['progress'],
'@total' => $sandbox['max'],
]);
}
}
/**
* Implements hook_update_N().
*
* Add parent_entity_type_id and parent_entity_id
* base fields to store a link to the entity.
*/
function linkchecker_update_8005() {
// Entity type id related to the link.
$entity_type_id_definition = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Entity Type id'))
->setDescription(new TranslatableMarkup('The entity type id string of the entity in which link was found.'))
->setRequired(TRUE);
// Entity id related to the link.
$entity_id_definition = BaseFieldDefinition::create('integer')
->setLabel(new TranslatableMarkup('Entity ID'))
->setDescription(new TranslatableMarkup('The entity id integer of the entity in which link was found.'))
->setRequired(TRUE);
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_definition_update_manager->installFieldStorageDefinition('parent_entity_type_id', 'linkcheckerlink', 'linkcheckerlink', $entity_type_id_definition);
$entity_definition_update_manager->installFieldStorageDefinition('parent_entity_id', 'linkcheckerlink', 'linkcheckerlink', $entity_id_definition);
}
/**
* Implements hook_update_N().
*
* Convert existing entity_id to new fields values.
*/
function linkchecker_update_8006(&$sandbox) {
$linkchecker_storage = \Drupal::entityTypeManager()->getStorage('linkcheckerlink');
// Firstly - initialize batch variables.
$items_per_batch = 20;
if (!isset($sandbox['progress'])) {
$sandbox['max'] = $linkchecker_storage->getQuery()
->accessCheck(FALSE)
->exists('entity_id')
->notExists('parent_entity_type_id')
->notExists('parent_entity_id')
->count()
->execute();
$sandbox['progress'] = 0;
}
$linkchecker_ids = $linkchecker_storage->getQuery()
->accessCheck(FALSE)
->exists('entity_id')
->notExists('parent_entity_type_id')
->notExists('parent_entity_id')
->range(0, $items_per_batch)
->sort('lid')
->execute();
if (!empty($linkchecker_ids)) {
foreach ($linkchecker_storage->loadMultiple($linkchecker_ids) as $linkchecker_link) {
// Access field value via property cuz we already removed entity_id
// base field definition.
$entity_id_val = reset($linkchecker_link->entity_id);
$linkchecker_link->set('parent_entity_type_id', $entity_id_val['target_type']);
$linkchecker_link->set('parent_entity_id', $entity_id_val['target_id']);
$linkchecker_link->save();
$sandbox['progress']++;
}
}
$sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']);
// Show the progress until we finish updating all the entities.
if ($sandbox['#finished']) {
return t('Linkchecker links are updating: finished @progress of @total.', [
'@progress' => $sandbox['progress'],
'@total' => $sandbox['max'],
]);
}
}
/**
* Implements hook_update_N().
*
* Uninstall entity_id storage definition.
*/
function linkchecker_update_8007(&$sandbox) {
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$definition = $entity_definition_update_manager->getFieldStorageDefinition('entity_id', 'linkcheckerlink');
if ($definition !== NULL) {
$entity_definition_update_manager->uninstallFieldStorageDefinition($definition);
return t('The entity_id field storage definition successfully uninstalled.');
}
}
......@@ -15,8 +15,8 @@ use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\linkchecker\LinkCheckerLinkInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
......@@ -32,25 +32,25 @@ function linkchecker_help($route_name, RouteMatchInterface $route_match) {
/**
* Conditionally logs a system message.
*
* @param $type
* @param string $type
* The category to which this message belongs. Can be any string, but the
* general practice is to use the name of the module calling watchdog().
* @param $message
* @param string $message
* The message to store in the log. Keep $message translatable
* by not concatenating dynamic values into it! Variables in the
* message should be added by using placeholder strings alongside
* the variables argument to declare the value of the placeholders.
* See t() for documentation on how $message and $variables interact.
* @param $variables
* @param array $variables
* Array of variables to replace in the message on display or
* NULL if message is already translated or not possible to
* translate.
* @param $severity
* The severity of the message; one of the following values as defined in
* @param $link
* @param string $severity
* The severity of the message; one of the following values as defined in.
* @param string $link
* A link to associate with the message.
*
* @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink
* @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink
* - WATCHDOG_EMERGENCY: Emergency, system is unusable.
* - RfcLogLevel::ALERT: Alert, action must be taken immediately.
* - RfcLogLevel::CRITICAL: Critical conditions.
......@@ -59,10 +59,12 @@ function linkchecker_help($route_name, RouteMatchInterface $route_match) {
* - RfcLogLevel::NOTICE: (default) Normal but significant conditions.
* - WATCHDOG_INFO: Informational messages.
* - WATCHDOG_DEBUG: Debug-level messages.
*
* @see watchdog_severity_levels()
*
* @see watchdog()
*/
function linkchecker_watchdog_log($type, $message, $variables = [], $severity = RfcLogLevel::NOTICE, $link = NULL) {
function linkchecker_watchdog_log($type, $message, array $variables = [], $severity = RfcLogLevel::NOTICE, $link = NULL) {
// @FIXME: $link is missing, could be in $variables.
if ($severity <= \Drupal::config('linkchecker.settings')->get('logging.level')) {
$logger = \Drupal::logger($type);
......@@ -127,10 +129,14 @@ function linkchecker_form_field_config_form_alter(&$form, FormStateInterface $fo
$form['#entity_builders'][] = 'linkchecker_form_field_config_form_builder';
}
/**
* Perform config form building.
*/
function linkchecker_form_field_config_form_builder($entity_type, FieldConfigInterface $field_config, &$form, FormStateInterface $form_state) {
if ($form_state->getValue(['third_party_settings', 'linkchecker', 'scan']) === 1) {
$field_config->setThirdPartySetting('linkchecker', 'scan', TRUE);
$field_config->setThirdPartySetting('linkchecker', 'extractor', $form_state->getValue(['third_party_settings', 'linkchecker', 'extractor']));
$field_config->setThirdPartySetting('linkchecker', 'extractor',
$form_state->getValue(['third_party_settings', 'linkchecker', 'extractor']));
return;
}
......@@ -290,7 +296,7 @@ function linkchecker_migrate_prepare_row(Row $row, MigrateSourceInterface $sourc
->condition('name', 'linkchecker_scan_' . $entity_type . '_' . $bundle)
->execute()
->fetchField();
$linkchecker_enabled = $result !== FALSE ? unserialize($result) : FALSE;
$linkchecker_enabled = $result !== FALSE ? unserialize($result, ['allowed_classes' => FALSE]) : FALSE;
if ($linkchecker_enabled) {
$row->setSourceProperty(
'linkchecker_config',
......
......@@ -24,6 +24,7 @@ services:
- '@linkchecker.extractor'
- '@entity_type.manager'
- '@database'
- '@config.factory'
linkchecker.checker:
class: Drupal\linkchecker\LinkCheckerService
......
......@@ -3,12 +3,12 @@
namespace Drupal\linkchecker\Commands;
use Drupal\Core\Config\ConfigFactoryInterface;
use Psr\Log\LoggerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\linkchecker\LinkCheckerBatch;
use Drupal\linkchecker\LinkCleanUp;
use Drupal\linkchecker\LinkExtractorBatch;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;
/**
* Drush 10 commands for Linkchecker module.
......@@ -57,11 +57,13 @@ class LinkCheckerCommands extends DrushCommands {
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* A config factory for retrieving required config objects.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
* @param \Drupal\linkchecker\LinkExtractorBatch $extractorBatch
* The extractor batch helper.
* @param \Drupal\linkchecker\LinkCheckerBatch $checkerBatch
* The checker batch helper.
* @param \Drupal\linkchecker\LinkCleanUp $linkCleanUp
* @param \Drupal\linkchecker\LinkCleanUp $linkCleanUp
* The link clean up.
*/
public function __construct(ConfigFactoryInterface $configFactory, LoggerInterface $logger, LinkExtractorBatch $extractorBatch, LinkCheckerBatch $checkerBatch, LinkCleanUp $linkCleanUp) {
......@@ -154,11 +156,11 @@ class LinkCheckerCommands extends DrushCommands {
}
if (empty($baseUrl)) {
throw new \Exception($this->t('You MUST configure the site base_url or provide --uri parameter.'));
throw new \Exception('You MUST configure the site base_url or provide --uri parameter.');
}
if (mb_strpos($baseUrl, 'http') !== 0) {
throw new \Exception($this->t('Base url should start with http scheme (http:// or https://)'));
throw new \Exception('Base url should start with http scheme (http:// or https://)');
}
}
......
......@@ -43,6 +43,7 @@ use Drupal\linkchecker\LinkCheckerLinkInterface;
* admin_permission = "administer linkchecker",
* entity_keys = {
* "id" = "lid",
* "uuid" = "uuid",
* "published" = "status"
* },
* links = {
......@@ -180,15 +181,17 @@ class LinkCheckerLink extends ContentEntityBase implements LinkCheckerLinkInterf
* {@inheritdoc}
*/
public function getParentEntity() {
return $this->get('entity_id')->entity;
$entity_type_id = $this->get('parent_entity_type_id')->getString();
$entity_id = $this->get('parent_entity_id')->getString();
return $this->entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
}
/**
* {@inheritdoc}
*/
public function setParentEntity(FieldableEntityInterface $entity) {
$this->get('entity_id')->target_id = $entity->id();
$this->get('entity_id')->target_type = $entity->getEntityTypeId();
$this->set('parent_entity_type_id', $entity->getEntityTypeId());
$this->set('parent_entity_id', $entity->id());
return $this;
}
......@@ -264,6 +267,12 @@ class LinkCheckerLink extends ContentEntityBase implements LinkCheckerLinkInterf
$fields[$entity_type->getKey('published')]->setLabel(new TranslatableMarkup('Check link status'));
// UUID field, required for JSON:API.
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(new TranslatableMarkup('UUID'))
->setDescription(new TranslatableMarkup('The entity UUID.'))
->setReadOnly(TRUE);
// Hash of URL.
$fields['urlhash'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('URL hash'))
......@@ -307,10 +316,16 @@ class LinkCheckerLink extends ContentEntityBase implements LinkCheckerLinkInterf
->setLabel(new TranslatableMarkup('Last checked'))
->setDescription(new TranslatableMarkup('Timestamp of the last link check.'));
// Entity type id related to the link.
$fields['parent_entity_type_id'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Entity Type id'))
->setDescription(new TranslatableMarkup('The entity type id string of the entity in which link was found.'))
->setRequired(TRUE);
// Entity id related to the link.
$fields['entity_id'] = BaseFieldDefinition::create('dynamic_entity_reference')
->setLabel(new TranslatableMarkup('Entity id'))
->setDescription(new TranslatableMarkup('ID of entity in which link was found.'))
$fields['parent_entity_id'] = BaseFieldDefinition::create('integer')
->setLabel(new TranslatableMarkup('Entity ID'))
->setDescription(new TranslatableMarkup('The entity id integer of the entity in which link was found.'))
->setRequired(TRUE);
// Entity field related to the link.
......
......@@ -3,10 +3,13 @@
namespace Drupal\linkchecker\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\filter\FilterPluginCollection;
use Drupal\filter\FilterPluginManager;
......@@ -72,11 +75,25 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
*/
protected $linkCheckerResponseCodes;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* ModuleHandler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* LinkCheckerAdminSettingsForm constructor.
*/
public function __construct(ConfigFactoryInterface $config_factory, DateFormatterInterface $date_formatter, FilterPluginManager $plugin_manager_filter, LinkCheckerService $linkchecker_checker, LinkExtractorBatch $extractorBatch, LinkCleanUp $linkCleanUp, UserStorageInterface $user_storage, LinkCheckerResponseCodesInterface $linkCheckerResponseCodes) {
parent::__construct($config_factory);
public function __construct(ConfigFactoryInterface $config_factory, DateFormatterInterface $date_formatter, FilterPluginManager $plugin_manager_filter, LinkCheckerService $linkchecker_checker, LinkExtractorBatch $extractorBatch, LinkCleanUp $linkCleanUp, UserStorageInterface $user_storage, LinkCheckerResponseCodesInterface $linkCheckerResponseCodes, ModuleHandlerInterface $module_handler, AccountInterface $current_user, TypedConfigManagerInterface $typed_config_manager) {
parent::__construct($config_factory, $typed_config_manager);
$this->dateFormatter = $date_formatter;
$this->extractorBatch = $extractorBatch;
$this->linkCleanUp = $linkCleanUp;
......@@ -84,6 +101,8 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
$this->filterPluginManager = $plugin_manager_filter;
$this->userStorage = $user_storage;
$this->linkCheckerResponseCodes = $linkCheckerResponseCodes;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
}
/**
......@@ -98,7 +117,10 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
$container->get('linkchecker.extractor_batch'),
$container->get('linkchecker.clean_up'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('linkchecker.response_codes')
$container->get('linkchecker.response_codes'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('config.typed')
);
}
......@@ -173,8 +195,8 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
'#title' => $this->t('Default URL scheme'),
'#description' => $this->t('Default URL scheme for scheme relative paths'),
'#options' => [
'http://' => 'HTTP',
'https://' => 'HTTPS',
'http://' => $this->t('HTTP'),
'https://' => $this->t('HTTPS'),
],
];
$form['general']['base_path'] = [
......@@ -184,6 +206,12 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
'#description' => $this->t('Should not start with URL scheme'),
];
$form['general']['search_published_contents_only'] = [
'#default_value' => $config->get('search_published_contents_only'),
'#type' => 'checkbox',
'#title' => $this->t('Search published contents only'),
'#description' => $this->t('If selected, links in unpublished content will be ignored.'),
];
$form['tag'] = [
'#type' => 'details',
'#title' => $this->t('Link extraction'),
......@@ -223,7 +251,7 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
'#default_value' => $config->get('extract.from_object'),
'#type' => 'checkbox',
'#title' => $this->t('Extract links in <code>&lt;object&gt;</code> and <code>&lt;param&gt;</code> tags'),
'#description' => $this->t('Enable this checkbox if multimedia and other links in object and their param tags should be extracted. The object tag is used for flash, java, quicktime and other applets.'),
'#description' => $this->t('Enable this checkbox if multimedia and other links in object and their param tags should be extracted. The object tag is used for flash, java, quick time and other applets.'),
];
$form['tag']['linkchecker_extract_from_video'] = [
'#default_value' => $config->get('extract.from_video'),
......@@ -291,11 +319,11 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
'#description' => $this->t('Defines the user agent that will be used for checking links on remote sites. If someone blocks the standard Drupal user agent you can try with a more common browser.'),
'#default_value' => $config->get('check.useragent'),
'#options' => [
'Drupal (+https://drupal.org/)' => 'Drupal (+https://drupal.org/)',
'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko' => 'Windows 8.1 (x64), Internet Explorer 11.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586' => 'Windows 10 (x64), Edge',
'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => 'Windows 8.1 (x64), Mozilla Firefox 47.0',
'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => 'Windows 10 (x64), Mozilla Firefox 47.0',
'Drupal (+https://drupal.org/)' => $this->t('Drupal (+https://drupal.org/)'),
'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko' => $this->t('Windows 8.1 (x64), Internet Explorer 11.0'),
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586' => $this->t('Windows 10 (x64), Edge'),
'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => $this->t('Windows 8.1 (x64), Mozilla Firefox 47.0'),
'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => $this->t('Windows 10 (x64), Mozilla Firefox 47.0'),
],
];
$intervals = [
......@@ -368,8 +396,17 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
10 => $this->t('After ten failed checks'),
],
];
if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
$form['error']['#description'] = $this->t('If enabled, outdated links in content providing a status <em>Moved Permanently</em> (status code 301) are automatically updated to the most recent URL. If used, it is recommended to use a value of <em>three</em> to make sure this is not only a temporarily change. This feature trust sites to provide a valid permanent redirect. A new content revision is automatically created on link updates if <em>create new revision</em> is enabled in the <a href=":content_types">content types</a> publishing options. It is recommended to create new revisions for all link checker enabled content types. Link updates are nevertheless always logged in <a href=":dblog">recent log entries</a>.', [':dblog' => Url::fromRoute('entity.node_type.collection')->toString(), ':content_types' => Url::fromRoute('entity.node_type.collection')->toString()]);
if ($this->moduleHandler->moduleExists('dblog') && $this->currentUser->hasPermission('access site reports')) {
$form['error']['#description'] = $this->t('If enabled, outdated links in content providing a status
<em>Moved Permanently</em> (status code 301) are automatically updated to the most recent URL. If used,
it is recommended to use a value of <em>three</em> to make sure this is not only a temporarily change.
This feature trust sites to provide a valid permanent redirect.
A new content revision is automatically created on link updates if <em>create new revision</em> is enabled in the <a href=":content_types">content types</a> publishing options.
It is recommended to create new revisions for all link checker enabled content types. Link updates are nevertheless always logged in <a href=":dblog">recent log entries</a>.',
[
':dblog' => Url::fromRoute('entity.node_type.collection')->toString(),
':content_types' => Url::fromRoute('entity.node_type.collection')->toString(),
]);
}
else {
$form['error']['#description'] = $this->t('If enabled, outdated links in content providing a status <em>Moved Permanently</em> (status code 301) are automatically updated to the most recent URL. If used, it is recommended to use a value of <em>three</em> to make sure this is not only a temporarily change. This feature trust sites to provide a valid permanent redirect. A new content revision is automatically created on link updates if <em>create new revision</em> is enabled in the <a href=":content_types">content types</a> publishing options. It is recommended to create new revisions for all link checker enabled content types. Link updates are nevertheless always logged.', [':content_types' => Url::fromRoute('entity.node_type.collection')->toString()]);
......@@ -442,7 +479,7 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
// Validate impersonation user name.
$linkchecker_impersonate_account = user_load_by_name($form_state->getValue('linkchecker_impersonate_account'));
// @TODO: Cleanup
// @todo Cleanup
// if (empty($linkchecker_impersonate_account->id())) {
if ($linkchecker_impersonate_account && empty($linkchecker_impersonate_account->id())) {
$form_state->setErrorByName('linkchecker_impersonate_account', $this->t('User account %name cannot found.', ['%name' => $form_state->getValue('linkchecker_impersonate_account')]));
......@@ -455,7 +492,7 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('linkchecker.settings');
// @todo: Move it to setting save hook.
// @todo Move it to setting save hook.
if ((int) $config->get('check.connections_max') != (int) $form_state->getValue('linkchecker_check_connections_max')) {
$this->linkCheckerService->queueLinks(TRUE);
}
......@@ -478,6 +515,7 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase {
->set('check.library', $form_state->getValue('linkchecker_check_library'))
->set('check.interval', $form_state->getValue('linkchecker_check_interval'))
->set('check.useragent', $form_state->getValue('linkchecker_check_useragent'))
->set('search_published_contents_only', $form_state->getValue('search_published_contents_only'))
->set('error.action_status_code_301', $form_state->getValue('linkchecker_action_status_code_301'))
->set('error.action_status_code_404', $form_state->getValue('linkchecker_action_status_code_404'))
->set('error.ignore_response_codes', $form_state->getValue('linkchecker_ignore_response_codes'))
......
......@@ -50,7 +50,7 @@ class LinkCheckerLinkAccessControlHandler extends EntityAccessControlHandler {
}
}
// User not allowed to view URL field if he does not have access to parent
// User not allowed to view URL field if it does not have access to parent
// entity.
if ($operation == 'view'
&& isset($items)
......
......@@ -6,7 +6,7 @@ use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
/**
* Defines the linkcheckerfile schema handler.
* Defines storage schema handler for the linkcheckerlink entity type.
*/
class LinkCheckerLinkStorageSchema extends SqlContentEntityStorageSchema {
......
......@@ -14,12 +14,12 @@ use Drupal\linkchecker\Event\BuildHeader;
use Drupal\linkchecker\Event\LinkcheckerEvents;
use Drupal\linkchecker\Plugin\LinkStatusHandlerManager;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class LinkCheckerService.
* Created a Class for creating Services.
*/
class LinkCheckerService {
......@@ -95,13 +95,13 @@ class LinkCheckerService {
}
/**
* Queue all links for checking.
* Queue all published links for checking.
*
* @param bool $rebuild
* Defines whether rebuild queue or not.
* Defines whether to rebuild queue or not.
*
* @return int
* Nubmer of queued items.
* Number of queued items.
*/
public function queueLinks($rebuild = FALSE) {
if ($rebuild) {
......@@ -113,7 +113,10 @@ class LinkCheckerService {
}
$checkInterval = $this->linkcheckerSetting->get('check.interval');
$query = $this->entityTypeManager->getStorage('linkcheckerlink')->getAggregateQuery()->accessCheck();
$query = $this->entityTypeManager->getStorage('linkcheckerlink')
->getAggregateQuery()
->accessCheck()
->condition('status', 1);
$orGroup = $query->orConditionGroup()
->condition('last_check', $this->time->getRequestTime() - $checkInterval, '<=')
->condition('last_check', NULL, 'IS NULL');
......@@ -127,8 +130,8 @@ class LinkCheckerService {
if (!empty($linkIds)) {
$linkIds = array_column($linkIds, 'lid_min');
$maxConnections = $this->linkcheckerSetting->get('check.connections_max');
// Split ids by max connection amount to make possible send concurrent
// requests.
// Split ids by max connection amount to make it possible to send
// concurrent requests.
$linkIds = array_chunk($linkIds, $maxConnections);
}
else {
......
......@@ -20,12 +20,16 @@ class LinkCheckerStorage extends SqlContentEntityStorage {
* An array of IDs, or an empty array if not found.
*/
public function getExistingIdsFromLink(LinkCheckerLink $link) {
$query = $this->getQuery();
$query->accessCheck()
$parent_entity = $link->getParentEntity();
if ($parent_entity === NULL) {
return [];
}
$query = $this
->getQuery()
->accessCheck(TRUE)
->condition('urlhash', LinkCheckerLink::generateHash($link->getUrl()))
->condition('entity_id.target_id', $link->getParentEntity()->id())
->condition('entity_id.target_type', $link->getParentEntity()
->getEntityTypeId())
->condition('parent_entity_type_id', $parent_entity->getEntityTypeId())
->condition('parent_entity_id', $parent_entity->id())
->condition('entity_field', $link->getParentEntityFieldName())
->condition('entity_langcode', $link->getParentEntityLangcode());
return $query->execute();
......
......@@ -4,13 +4,15 @@ namespace Drupal\linkchecker;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Class LinkCleanUp.
* Form to clean the links.
*/
class LinkCleanUp {
......@@ -79,7 +81,7 @@ class LinkCleanUp {
}
/**
* Proccess link deletion within batch operation.
* Process link deletion within batch operation.
*
* @param mixed $context
* Batch context.
......@@ -95,7 +97,7 @@ class LinkCleanUp {
$ids = $storage->getQuery()
->accessCheck()
->range(0, 10)
->range(0, Settings::get('entity_update_batch_size', 50))
->execute();
$this->processDelete($ids);
......@@ -149,8 +151,8 @@ class LinkCleanUp {
// Get list of link IDs that should be deleted.
$query = $storage->getQuery();
$query->accessCheck();
$query->condition('entity_id.target_id', $entity->id());
$query->condition('entity_id.target_type', $entity->getEntityTypeId());
$query->condition('parent_entity_type_id', $entity->getEntityTypeId());
$query->condition('parent_entity_id', $entity->id());
if (!empty($extractedIds)) {
$query->condition('lid', $extractedIds, 'NOT IN');
}
......
......@@ -3,12 +3,15 @@
namespace Drupal\linkchecker;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Helper service to handle extraction index.
......@@ -40,13 +43,21 @@ class LinkExtractorBatch {
*/
protected $database;
/**
* The Linkchecker settings.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $linkcheckerSetting;
/**
* LinkExtractorBatch constructor.
*/
public function __construct(LinkExtractorService $extractor, EntityTypeManagerInterface $entityTypeManager, Connection $dbConnection) {
public function __construct(LinkExtractorService $extractor, EntityTypeManagerInterface $entityTypeManager, Connection $dbConnection, ConfigFactoryInterface $configFactory) {
$this->extractor = $extractor;
$this->entityTypeManager = $entityTypeManager;
$this->database = $dbConnection;
$this->linkcheckerSetting = $configFactory->get('linkchecker.settings');
}
/**
......@@ -56,9 +67,10 @@ class LinkExtractorBatch {
* List of entity types with bundles.
*/
public function getEntityTypesToProcess() {
$fieldConfigs = $this->entityTypeManager
$fieldConfigs = $this
->entityTypeManager
->getStorage('field_config')
->loadMultiple(NULL);
->loadMultiple();
$entityTypes = [];
/** @var \Drupal\Core\Field\FieldConfigInterface $config */
......@@ -90,30 +102,40 @@ class LinkExtractorBatch {
* @return int
* Number of items that were processed.
*/
public function processEntities($numberOfItems = 20) {
$entityTypes = $this->getEntityTypesToProcess();
public function processEntities($numberOfItems = NULL) {
$numberOfProcessedItems = 0;
// This function is used in batch to extract all links on demand and it's
// also called on every cron run (see linkchecker_cron()). Because it uses
// SQL LEFT JOIN, it's quite expensive. So, first check if there is anything
// to process. If yes, then use query with LEFT JOIN to retrieve entities to
// be processed.
if ($this->getTotalEntitiesToProcess() <= $this->getNumberOfProcessedEntities()) {
return $numberOfProcessedItems;
}
if (!$numberOfItems) {
$numberOfItems = Settings::get('entity_update_batch_size', 50);
}
$entityTypes = $this->getEntityTypesToProcess();
foreach ($entityTypes as $entityTypeData) {
/** @var \Drupal\Core\Entity\EntityTypeInterface $entityType */
$entityType = $entityTypeData['entity_type'];
$bundle = $entityTypeData['bundle'];
$query = $this->database->select($entityType->getBaseTable(), 'base');
$query->fields('base', [$entityType->getKey('id')]);
$query = $this->getQuery($entityType, $bundle);
$query->leftJoin('linkchecker_index', 'i', 'i.entity_id = base.' . $entityType->getKey('id') . ' AND i.entity_type = :entity_type', [
':entity_type' => $entityType->id(),
]);
$query->isNull('i.entity_id');
if (!empty($bundle)) {
$query->condition('base.' . $entityType->getKey('bundle'), $bundle);
}
$query->range(0, $numberOfItems - $numberOfProcessedItems);
$ids = $query->execute()->fetchCol();
$storage = $this->entityTypeManager->getStorage($entityType->id());
foreach ($ids as $id) {
$entity = $storage->load($id);
$entities = $this
->entityTypeManager
->getStorage($entityType->id())
->loadMultiple($ids);
foreach ($entities as $entity) {
if ($entity instanceof FieldableEntityInterface) {
// Process the entity links.
$links = $this->extractor->extractFromEntity($entity);
......@@ -146,15 +168,7 @@ class LinkExtractorBatch {
/** @var \Drupal\Core\Entity\EntityTypeInterface $entityType */
$entityType = $entityTypeData['entity_type'];
$bundle = $entityTypeData['bundle'];
// We don`t use $this->getQuery() cause we do not need left join
// on linkchecker_index table.
$query = $this->database->select($entityType->getBaseTable(), 'base');
$query->fields('base', [$entityType->getKey('id')]);
if (!empty($bundle)) {
$query->condition('base.' . $entityType->getKey('bundle'), $bundle);
}
$query = $this->getQuery($entityType, $bundle);
$query = $query->countQuery();
$total += $query->execute()->fetchField();
}
......@@ -186,7 +200,7 @@ class LinkExtractorBatch {
$batch = new BatchBuilder();
$batch->setTitle('Extract entities')
->addOperation([$this, 'batchProcessEntities'], [20])
->addOperation([$this, 'batchProcessEntities'], [Settings::get('entity_update_batch_size', 50)])
->setProgressive()
->setFinishCallback([$this, 'batchFinished']);
......@@ -231,4 +245,44 @@ class LinkExtractorBatch {
}
}
/**
* Get link extraction query.
*
* If unpublished content should be skipped, data table is used, base table
* otherwise.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type.
* @param string $bundle
* The bundle.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* The select query.
*/
protected function getQuery(EntityTypeInterface $entityType, $bundle) {
// Use data table only if exists and unpublished content should be skipped.
if ($entityType->hasKey('status')
&& $entityType->getDataTable()
&& $this->linkcheckerSetting->get('search_published_contents_only')
) {
$query = $this
->database
->select($entityType->getDataTable(), 'base')
->condition('base.' . $entityType->getKey('status'), 1)
->distinct();
}
// Otherwise, use base table, it has all necessary information.
else {
$query = $this->database->select($entityType->getBaseTable(), 'base');
}
$query->fields('base', [$entityType->getKey('id')]);
if (!empty($bundle)) {
$query->condition('base.' . $entityType->getKey('bundle'), $bundle);
}
$query->orderBy('base.' . $entityType->getKey('id'));
return $query;
}
}