diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f71749cb284d9412caeb0910e82113ae5f3b35b2..66b7952b8507e163e94d39155a7fc1a71a538307 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,23 +22,9 @@ variables: _PHPUNIT_CONCURRENT: "1" _PHPUNIT_EXTRA: "--suppress-deprecations" OPT_IN_TEST_NEXT_MAJOR: '0' - # Not Drupal 11 compatable yet. OPT_IN_TEST_CURRENT: '1' OPT_IN_TEST_PREVIOUS_MAJOR: '1' -# Rules for PHPStan on Drupal 10. -.opt-in-previous-major-rule: &opt-in-previous-major-rule - if: '$OPT_IN_TEST_PREVIOUS_MAJOR != "1" || $CORE_PREVIOUS_STABLE == ""' - when: never -.skip-phpstan-rule: &skip-phpstan-rule - if: '$SKIP_PHPSTAN == "1"' - when: never -.php-files-exist-rule: &php-files-exist-rule - - exists: - - "*.{$DRUPAL_PHP_FILE_TYPES}" - - "**/*.{$DRUPAL_PHP_FILE_TYPES}" - when: on_success - # # Linting jobs are passing so any issue that breaks them should fix them. # @@ -52,15 +38,7 @@ phpcs: - vendor/bin/phpcs -s $_WEB_ROOT/modules/custom --report-junit=junit.xml --report-full --report-summary --report-source allow_failure: false phpstan: - # Allow PHPStan to run on Drupal 10. - rules: - - *opt-in-previous-major-rule - - *skip-phpstan-rule - - *php-files-exist-rule - needs: - - "composer (previous major)" allow_failure: false - phpstan (next major): allow_failure: true stylelint: diff --git a/entity_usage_updater.info.yml b/entity_usage_updater.info.yml index 2c85bd2cdec24573113b39fad17e559a646cad66..c874335776648ace9f382a38c9d3303c210bd81b 100644 --- a/entity_usage_updater.info.yml +++ b/entity_usage_updater.info.yml @@ -1,7 +1,7 @@ name: Entity Usage Updater type: module description: Lets you update entity references tracked by the Entity Usage module. -core_version_requirement: ^10 || ^11 +core_version_requirement: ^10.2 || ^11 dependencies: - entity_usage:entity_usage (8.x-2.x) test_dependencies: diff --git a/entity_usage_updater.services.yml b/entity_usage_updater.services.yml index 50c0419aba55de2aed8991f6075f9176b8db3235..9777a92f15f1b77221e55789c165fcd95bfd320c 100644 --- a/entity_usage_updater.services.yml +++ b/entity_usage_updater.services.yml @@ -1,4 +1,7 @@ services: + _defaults: + autoconfigure: true + autowire: true entity_usage_updater.updater: class: Drupal\entity_usage_updater\EntityUsageUpdater @@ -10,3 +13,5 @@ services: plugin.manager.entity_usage_updater: class: Drupal\entity_usage_updater\EntityUsageUpdaterPluginManager parent: default_plugin_manager + + Drupal\entity_usage_updater\Event\EntityUsageUpdaterControllerSubscriber: ~ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3d75900371367abae2721ee7e65cf050633479df..a2fdbc246acc487450bb4d50ece255c9fe212e22 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,21 +1,12 @@ parameters: ignoreErrors: - - - message: "#^Call to an undefined method Drupal\\\\Core\\\\Entity\\\\EntityInterface\\:\\:getTranslation\\(\\)\\.$#" - count: 1 - path: src/EntityUsageUpdater.php - - message: "#^Call to an undefined method Drupal\\\\Core\\\\Entity\\\\EntityStorageInterface\\:\\:getLatestRevisionId\\(\\)\\.$#" count: 1 path: src/EntityUsageUpdater.php - - message: """ - #^Call to deprecated method loadRevision\\(\\) of interface Drupal\\\\Core\\\\Entity\\\\EntityStorageInterface\\: - in drupal\\:10\\.1\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use - \\\\Drupal\\\\Core\\\\Entity\\\\RevisionableStorageInterface\\:\\:loadRevision instead\\.$# - """ + message: "#^Call to an undefined method Drupal\\\\Core\\\\Entity\\\\EntityStorageInterface\\:\\:loadRevision\\(\\)\\.$#" count: 1 path: src/EntityUsageUpdater.php diff --git a/src/Controller/LocalTaskUsageController.php b/src/Controller/LocalTaskUsageController.php new file mode 100644 index 0000000000000000000000000000000000000000..d0115344da02094620378dc3554580542d1784b9 --- /dev/null +++ b/src/Controller/LocalTaskUsageController.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\entity_usage_updater\Controller; + +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\entity_usage\Controller\LocalTaskUsageController as BaseLocalTaskUsageController; +use Drupal\entity_usage_updater\Form\EntityUsageUpdateForm; + +/** + * Controller to add form to entity usage local tasks. + */ +class LocalTaskUsageController extends BaseLocalTaskUsageController { + + /** + * {@inheritdoc} + */ + public function listUsagePage($entity_type, $entity_id): array { + $build = parent::listUsagePage($entity_type, $entity_id); + // Only add the form if there are usages. + if (array_keys($build) !== ['#markup']) { + $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); + $form = $this->formBuilder()->getForm(EntityUsageUpdateForm::class, $entity); + // @todo add ability to move the form? + $build['entity_usage_updater'] = [ + '#type' => 'details', + '#title' => t('Update usages'), + '#tree' => FALSE, + '#open' => FALSE, + ]; + $build['entity_usage_updater']['form'] = $form; + } + return $build; + } + + /** + * {@inheritdoc} + */ + public function listUsageLocalTask(RouteMatchInterface $route_match) { + $entity = $this->getEntityFromRouteMatch($route_match); + return $this->listUsagePage($entity->getEntityTypeId(), $entity->id()); + } + +} diff --git a/src/Event/EntityUsageUpdaterControllerSubscriber.php b/src/Event/EntityUsageUpdaterControllerSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..af82d899d6d71314858e89649ba61215ac778200 --- /dev/null +++ b/src/Event/EntityUsageUpdaterControllerSubscriber.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\entity_usage_updater\Event; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Routing\RouteSubscriberBase; +use Symfony\Component\Routing\RouteCollection; + +/** + * Swaps the entity usage tab controller for the one provided by this module. + */ +class EntityUsageUpdaterControllerSubscriber extends RouteSubscriberBase { + + public function __construct(protected EntityTypeManagerInterface $entityTypeManager) { + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection): void { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + $route = $collection->get("entity.$entity_type_id.entity_usage"); + if ($route) { + $route->setDefault('_controller', '\Drupal\entity_usage_updater\Controller\LocalTaskUsageController::listUsageLocalTask'); + } + } + + $route = $collection->get('entity_usage.usage_list'); + if ($route) { + $route->setDefault('_controller', '\Drupal\entity_usage_updater\Controller\LocalTaskUsageController::listUsagePage'); + } + } + +} diff --git a/src/Form/EntityUsageUpdateForm.php b/src/Form/EntityUsageUpdateForm.php index 4d1d8d88da4af672bed4be9c351f64b44f3e85a2..9a31b689c88826a90558fb35ac684b5c89a269c6 100644 --- a/src/Form/EntityUsageUpdateForm.php +++ b/src/Form/EntityUsageUpdateForm.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\entity_usage_updater\Form; use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; @@ -51,7 +52,7 @@ class EntityUsageUpdateForm extends FormBase { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state, ?EntityInterface $entity = NULL) { $is_content_entity_type = fn(EntityTypeInterface $type): bool => $type instanceof ContentEntityTypeInterface; $types = array_filter($this->entityTypeManager->getDefinitions(), $is_content_entity_type); $options = []; @@ -60,21 +61,33 @@ class EntityUsageUpdateForm extends FormBase { } asort($options, SORT_LOCALE_STRING); - $form['entity_type_id'] = [ - '#title' => $this->t("Entity type"), - '#type' => 'select', - '#options' => $options, - '#required' => TRUE, - '#default_value' => isset($options['node']) ? 'node' : NULL, - ]; - - $form['old_entity_id'] = [ - '#title' => $this->t("Target entity ID"), - '#description' => $this->t("Enter the ID of the entity you no longer want referenced."), - '#type' => 'textfield', - '#required' => TRUE, - '#size' => 20, - ]; + if ($entity) { + $form['entity_type_id'] = [ + '#type' => 'value', + '#value' => $entity->getEntityTypeId(), + ]; + $form['old_entity_id'] = [ + '#type' => 'value', + '#value' => $entity->id(), + ]; + } + else { + $form['entity_type_id'] = [ + '#title' => $this->t("Entity type"), + '#type' => 'select', + '#options' => $options, + '#required' => TRUE, + '#default_value' => isset($options['node']) ? 'node' : NULL, + ]; + + $form['old_entity_id'] = [ + '#title' => $this->t("Target entity ID"), + '#description' => $this->t("Enter the ID of the entity you no longer want referenced."), + '#type' => 'textfield', + '#required' => TRUE, + '#size' => 20, + ]; + } $form['new_entity_id'] = [ '#title' => $this->t("Replacement entity ID"), diff --git a/src/Plugin/EntityUsageUpdater/HtmlLink.php b/src/Plugin/EntityUsageUpdater/HtmlLink.php index 304caa51aa2a4825a375921e8bae528d333f97ff..ce4898869d206e97654fa148777d7694aa15571c 100644 --- a/src/Plugin/EntityUsageUpdater/HtmlLink.php +++ b/src/Plugin/EntityUsageUpdater/HtmlLink.php @@ -123,7 +123,7 @@ class HtmlLink extends EntityUsageUpdaterPluginBase implements ContainerFactoryP $this->updateItemProperty($old_target, $new_entity_type, $new_id, $summary); } } - catch (EntityUsageUpdaterException $e) { + catch (EntityUsageUpdaterException) { $host = $item->getEntity(); $context = [ '@host_entity_type' => $host->getEntityType()->getLabel(), @@ -177,9 +177,10 @@ class HtmlLink extends EntityUsageUpdaterPluginBase implements ContainerFactoryP $options = [ 'fragment' => $parts['fragment'], 'query' => $parts['query'], + 'absolute' => CoreUrlHelper::isExternal($href), ]; - if ($this->configuration['convert_to_linkit']) { + if ($this->configuration['convert_to_linkit'] ?? FALSE) { $pattern = '~<a\s+[^>]*href\s*=\s*([\'"])' . preg_quote($href, '~') . '\1(\s+[^>]*)?>~i'; $new_href = Url::fromRoute("entity.$new_entity_type.canonical", [$new_entity_type => $new_id], $options)->toString(); $uuid = $this->getUuid($new_entity_type, $new_id); @@ -236,7 +237,7 @@ class HtmlLink extends EntityUsageUpdaterPluginBase implements ContainerFactoryP yield [$entity, $element]; } } - catch (\DOMException $e) { + catch (\DOMException) { // Do nothing. } } diff --git a/tests/src/Functional/ListControllerTest.php b/tests/src/Functional/ListControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..20aa4a9601a7862f6349f7c4435151013d2c2c6a --- /dev/null +++ b/tests/src/Functional/ListControllerTest.php @@ -0,0 +1,150 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\entity_usage_updater\Functional; + +use Drupal\filter\Entity\FilterFormat; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\entity_usage\Traits\EntityUsageLastEntityQueryTrait; + +/** + * Tests the tab listing the usage of a given entity with this module installed. + * + * @group entity_usage_updater + */ +class ListControllerTest extends BrowserTestBase { + + protected $defaultTheme = 'stark'; + + use EntityUsageLastEntityQueryTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'node', + 'field_ui', + 'text', + 'path', + 'entity_usage_updater', + ]; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + + $account = $this->drupalCreateUser([ + 'access entity usage statistics', + 'administer nodes', + 'bypass node access', + ]); + $this->drupalLogin($account); + + $this->drupalCreateContentType(['type' => 'page']); + + // Set up the filter formats used by this test. + $basic_html_format = FilterFormat::create([ + 'format' => 'basic_html', + 'name' => 'Basic HTML', + 'filters' => [ + 'filter_html' => [ + 'status' => 1, + 'settings' => [ + 'allowed_html' => '<p> <br> <strong> <a href> <em>', + ], + ], + ], + ]); + $basic_html_format->save(); + user_role_grant_permissions('authenticated', [$basic_html_format->getPermissionName()]); + + $current_request = \Drupal::request(); + $config = \Drupal::configFactory()->getEditable('entity_usage.settings'); + $config->set('site_domains', [$current_request->getHttpHost() . $current_request->getBasePath()]); + $config->save(); + $this->config('entity_usage.settings')->set('local_task_enabled_entity_types', ['node'])->save(); + \Drupal::service('router.builder')->rebuild(); + } + + /** + * Tests the page listing the usage of entities. + * + * @covers \Drupal\entity_usage\Controller\ListUsageController::listUsagePage + */ + public function testListController(): void { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Create node 1. + $this->drupalGet('node/add/page'); + $page->fillField('title[0][value]', 'Node 1'); + $page->pressButton('Save'); + $assert_session->pageTextContains('Node 1 has been created.'); + /** @var \Drupal\node\NodeInterface $node1 */ + $node1 = $this->drupalGetNodeByTitle('Node 1'); + + // Create node 2. + $this->drupalGet('node/add/page'); + $page->fillField('title[0][value]', 'Node 2'); + $page->pressButton('Save'); + $assert_session->pageTextContains('Node 2 has been created.'); + $node2 = $this->drupalGetNodeByTitle('Node 2'); + + // Create node 3. + $this->drupalGet('node/add/page'); + $page->fillField('title[0][value]', 'Node 3'); + $page->fillField('body[0][value]', (string) $node1->toLink("Link to content", options: ['absolute' => TRUE])->toString()); + // $page->fillField('body[0][value]', 'Testing!!!!!'); + $page->pressButton('Save'); + $assert_session->pageTextContains('Node 3 has been created.'); + $node3 = $this->drupalGetNodeByTitle('Node 3'); + + $this->assertSession()->linkByHrefExists($node1->toUrl()->toString()); + $this->assertSession()->linkByHrefNotExists($node2->toUrl()->toString()); + + $this->drupalGet('node/1/usage'); + $this->assertSession()->fieldExists('new_entity_id')->setValue($node2->id()); + $this->assertSession()->buttonExists('Update')->press(); + // Process the batch. + $this->checkForMetaRefresh(); + + // Check to see if the link has been replaced. + $this->drupalGet('node/3'); + $this->assertSession()->linkByHrefExists($node2->toUrl()->toString()); + $this->assertSession()->linkByHrefNotExists($node1->toUrl()->toString()); + + $this->drupalGet('node/3/usage'); + // Ensure the form for updating is not present if there are no usages. + $this->assertSession()->pageTextContains('There are no recorded usages for entity of type: node with id: 3'); + $this->assertSession()->fieldNotExists('new_entity_id'); + + // Ensure that if usages only exist in old revisions then they are not + // replaced even though the form is present. + // @todo can we remove the form in if they are no current usages? + $this->drupalGet('node/1/usage'); + $this->assertSession()->fieldExists('new_entity_id')->setValue($node2->id()); + $this->assertSession()->buttonExists('Update')->press(); + // Process the batch. + $this->checkForMetaRefresh(); + // Check to see if the link has not been replaced. + $this->drupalGet('node/3'); + $this->assertSession()->linkByHrefExists($node2->toUrl()->toString()); + $this->assertSession()->linkByHrefNotExists($node1->toUrl()->toString()); + + // Ensure the non-tab entity usage page works too. + $this->drupalGet("admin/content/entity-usage/node/2"); + $this->assertSession()->fieldExists('new_entity_id')->setValue($node1->id()); + $this->assertSession()->buttonExists('Update')->press(); + // Process the batch. + $this->checkForMetaRefresh(); + // Check to see if the link has not been replaced. + $this->drupalGet('node/3'); + $this->assertSession()->linkByHrefExists($node1->toUrl()->toString()); + $this->assertSession()->linkByHrefNotExists($node2->toUrl()->toString()); + } + +}