diff --git a/config/optional/views.view.entity_mesh.yml b/config/optional/views.view.entity_mesh.yml index b08bbcb39223425050512f8cc6a145f25e9662bc..fbc496886f0e23e06d8eaefbec6d6b54d9820a4e 100644 --- a/config/optional/views.view.entity_mesh.yml +++ b/config/optional/views.view.entity_mesh.yml @@ -1490,10 +1490,7 @@ display: empty: false content: 'Showing @start - @end de @total' footer: { } - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } cache_metadata: max-age: -1 contexts: @@ -1522,10 +1519,7 @@ display: encoding: utf8 utf8_bom: '0' use_serializer_encode_only: false - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } path: admin/reports/entity-mesh/table displays: table: table @@ -2527,9 +2521,6 @@ display: use_ajax: false display_description: '' display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false ajax_history: { } path: admin/reports/entity-mesh/links menu: @@ -3331,9 +3322,6 @@ display: use_ajax: false display_description: '' display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false ajax_history: enable_history: false path: admin/reports/entity-mesh/table diff --git a/config/optional/views.view.entity_mesh_node.yml b/config/optional/views.view.entity_mesh_node.yml index 7a6c88e8a017099c602a81a790b544834007ecc1..d185c9380fa9f2d6936afeec7eb2ae9097a4ded2 100644 --- a/config/optional/views.view.entity_mesh_node.yml +++ b/config/optional/views.view.entity_mesh_node.yml @@ -4,7 +4,6 @@ dependencies: module: - entity_mesh - user - id: entity_mesh_node label: 'Entity mesh node' module: views @@ -833,7 +832,7 @@ display: support_desk: '0' contributor: '0' api_client: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -875,7 +874,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -917,7 +916,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -959,7 +958,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1043,7 +1042,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1065,7 +1064,7 @@ display: admin_label: '' plugin_id: string operator: in - value: { } + value: '' group: 1 exposed: true expose: @@ -1086,7 +1085,6 @@ display: administrator: '0' editor: '0' placeholder: '' - reduce: 0 is_grouped: false group_info: label: '' @@ -1212,7 +1210,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1254,7 +1252,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1338,7 +1336,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1490,10 +1488,7 @@ display: empty: false content: 'Showing @start - @end de @total' footer: { } - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } cache_metadata: max-age: -1 contexts: @@ -1509,6 +1504,55 @@ display: position: 1 display_options: fields: + subcategory: + id: subcategory + table: entity_mesh + field: subcategory + relationship: none + group_type: group + admin_label: '' + plugin_id: standard + label: '' + exclude: true + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true type: id: type table: entity_mesh @@ -2446,6 +2490,48 @@ display: fail: 'not found' validate_options: { } filters: + subcategory: + id: subcategory + table: entity_mesh + field: subcategory + relationship: none + group_type: group + admin_label: '' + plugin_id: subcategory_filter + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: subcategory_op + label: 'Subcategory' + description: '' + use_operator: false + operator: subcategory_op + operator_limit_selection: false + operator_list: { } + identifier: subcategory + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + editor: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } source_entity_type: id: source_entity_type table: entity_mesh @@ -2514,7 +2600,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -2556,7 +2642,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -2640,7 +2726,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -2724,7 +2810,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -2761,10 +2847,7 @@ display: use_ajax: true display_description: '' header: { } - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } displays: node_source_table: node_source_table inherit_exposed_filters: true @@ -2783,6 +2866,55 @@ display: position: 5 display_options: fields: + subcategory: + id: subcategory + table: entity_mesh + field: subcategory + relationship: none + group_type: group + admin_label: '' + plugin_id: standard + label: 'Subcategory' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true target_link_type: id: target_link_type table: entity_mesh @@ -3308,6 +3440,48 @@ display: fail: 'not found' validate_options: { } filters: + subcategory: + id: subcategory + table: entity_mesh + field: subcategory + relationship: none + group_type: group + admin_label: '' + plugin_id: subcategory_filter + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: subcategory_op + label: 'Subcategory' + description: '' + use_operator: false + operator: subcategory_op + operator_limit_selection: false + operator_list: { } + identifier: subcategory + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + editor: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } source_entity_type: id: source_entity_type table: entity_mesh @@ -3376,7 +3550,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -3418,7 +3592,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -3502,7 +3676,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -3544,7 +3718,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -3622,10 +3796,7 @@ display: plugin_id: result empty: true content: 'Displaying @start - @end of @total' - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } path: node/%node/entity_mesh menu: type: none @@ -4708,7 +4879,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -4750,7 +4921,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -4829,10 +5000,7 @@ display: use_ajax: true display_description: '' header: { } - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } displays: node_target_table: node_target_table inherit_exposed_filters: true @@ -5342,7 +5510,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -5384,7 +5552,7 @@ display: anonymous: '0' administrator: '0' editor: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -5461,10 +5629,7 @@ display: plugin_id: result empty: true content: 'Displaying @start - @end of @total' - display_extenders: - metatag_display_extender: - metatags: { } - tokenize: false + display_extenders: { } path: node/%node/entity_mesh/target menu: type: none diff --git a/config/schema/entity_mesh.views.schema.yml b/config/schema/entity_mesh.views.schema.yml index 96f6fb3f6be65ece910ab31aa66484ea6f8310d7..1188f062797d9b1b63af765829a678e870cc5b22 100644 --- a/config/schema/entity_mesh.views.schema.yml +++ b/config/schema/entity_mesh.views.schema.yml @@ -6,3 +6,41 @@ views.filter.source_entity_type_filter: type: views.filter.in_operator label: 'Source entity type filter' +views.filter.category_filter: + type: views.filter.in_operator + label: 'Category filter' + +views.filter.subcategory_filter: + type: views.filter.in_operator + label: 'Subcategory filter' + +views.filter.source_bundle_filter: + type: views.filter.in_operator + label: 'Source bundle filter' + +views.filter.source_langcode_filter: + type: views.filter.in_operator + label: 'Source langcode filter' + +views.filter.target_bundle_filter: + type: views.filter.in_operator + label: 'Target bundle filter' + +views.filter.target_langcode_filter: + type: views.filter.in_operator + label: 'Target langcode filter' + +views.filter.target_schema_filter: + type: views.filter.in_operator + label: 'Target schema filter' + mapping: + value: + type: sequence + label: 'Values' + sequence: + type: string + label: 'Value' + +views.filter.target_type_link_filter: + type: views.filter.in_operator + label: 'Target type link filter' diff --git a/entity_mesh.install b/entity_mesh.install index ba84ac04969320eb0e03f7b7f2f57ea626f75d83..96f7b1799e3823d51d56d8c17c02dd02e944fc1e 100644 --- a/entity_mesh.install +++ b/entity_mesh.install @@ -255,3 +255,17 @@ function entity_mesh_update_10006() { \Drupal::service('config.installer')->installOptionalConfig($config_source); return (string) new TranslatableMarkup("Views data export installed and configured on Entity Mesh report."); } + +/** + * Add field and filter subcategory in entity mesh node views. + */ +function entity_mesh_update_10007() { + // Update view. + \Drupal::configFactory()->getEditable('views.view.entity_mesh_node')->delete(); + + $path_to_module = \Drupal::service('extension.path.resolver')->getPath('module', 'entity_mesh'); + $config_path = $path_to_module . '/config/optional'; + $config_source = new FileStorage($config_path); + \Drupal::service('config.installer')->installOptionalConfig($config_source); + return (string) new TranslatableMarkup("Views data export installed and configured on Entity Mesh report."); +} diff --git a/entity_mesh.routing.yml b/entity_mesh.routing.yml index f05b035c609ad972ac5644ec3280c55502427c70..ecebd30b0d3ac354f2c31434af0526ff57710692 100644 --- a/entity_mesh.routing.yml +++ b/entity_mesh.routing.yml @@ -4,7 +4,7 @@ entity_mesh.batch_form: _title: 'Entity Mesh batch' _form: 'Drupal\entity_mesh\Form\BatchForm' requirements: - _permission: 'access entity_mesh report' + _permission: 'administer entity_mesh configuration' entity_mesh.settings_form: path: '/admin/config/system/entity-mesh/config' diff --git a/entity_mesh.services.yml b/entity_mesh.services.yml index c47337cac322a18cb9e8cc399edca742652fdd88..cb7b429b6e49523c0445b48ecf7dee873cb7b3ce 100644 --- a/entity_mesh.services.yml +++ b/entity_mesh.services.yml @@ -26,6 +26,7 @@ services: - '@account_switcher' - '@entity_mesh.language_negotiator_switcher' - '@module_handler' + - '@access_manager' entity_mesh.language_negotiator_switcher: class: Drupal\entity_mesh\Language\LanguageNegotiatorSwitcher arguments: ['@language_manager', '@module_handler', '@string_translation', '@entity_mesh.static_language_negotiator'] diff --git a/src/EntityRender.php b/src/EntityRender.php index 7aa5866b1d727352a1631384d2891e441895003a..305687ab19937034821b224619f04f6a983c45ca 100644 --- a/src/EntityRender.php +++ b/src/EntityRender.php @@ -4,6 +4,7 @@ namespace Drupal\entity_mesh; use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Component\Utility\DeprecationHelper; +use Drupal\Core\Access\AccessManager; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Database\StatementInterface; use Drupal\Core\Entity\EntityInterface; @@ -15,6 +16,7 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountSwitcherInterface; use Drupal\Core\Session\AnonymousUserSession; use Drupal\entity_mesh\Language\LanguageNegotiatorSwitcher; +use Drupal\taxonomy\TermInterface; /** * Service description. @@ -51,6 +53,13 @@ class EntityRender extends Entity { */ protected ModuleHandlerInterface $moduleHandler; + /** + * Access manager. + * + * @var \Drupal\Core\Access\AccessManager + */ + protected AccessManager $accessManager; + /** * Constructs a Menu object. * @@ -70,6 +79,8 @@ class EntityRender extends Entity { * The language negotiator switcher service. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * Module handler. + * @param \Drupal\Core\Access\AccessManager $access_manager + * Access manager. */ public function __construct( RepositoryInterface $entity_mesh_repository, @@ -80,6 +91,7 @@ class EntityRender extends Entity { AccountSwitcherInterface $account_switcher, LanguageNegotiatorSwitcher $language_negotiator_switcher, ModuleHandlerInterface $module_handler, + AccessManager $access_manager, ) { parent::__construct($entity_mesh_repository, $entity_type_manager, $language_manager, $config_factory); $this->renderer = $renderer; @@ -87,6 +99,7 @@ class EntityRender extends Entity { $this->type = 'entity_render'; $this->languageNegotiatorSwitcher = $language_negotiator_switcher; $this->moduleHandler = $module_handler; + $this->accessManager = $access_manager; } /** @@ -283,8 +296,12 @@ class EntityRender extends Entity { * The target. */ protected function processInternalHref(TargetInterface $target) { - $path = (string) $target->getPath(); + + if ($path === '') { + return; + } + $alias = $this->entityMeshRepository->getPathWithoutLangPrefix($path); $target->setEntityLangcode($this->entityMeshRepository->getLangcodeFromPath($path)); @@ -468,20 +485,36 @@ class EntityRender extends Entity { return; } - // It is a view route. - if (isset($route_match['view_id'])) { - $target->setEntityType('view'); - $target->setEntityId($route_match['view_id'] . '.' . $route_match['display_id']); + if (empty($route_match['_route'])) { + $target->setSubcategory('broken-link'); + return; + } + + // @todo Maybe here is possible to get as well the entity object + if (isset($route_match['view_id']) && isset($route_match['display_id'])) { + if ($route_match['view_id'] === 'taxonomy_term' + && isset($route_match['taxonomy_term']) + && $route_match['taxonomy_term'] instanceof TermInterface + ) { + $target->setEntityType('taxonomy_term'); + $target->setEntityId($route_match['taxonomy_term']->id()); + } + else { + $target->setEntityType('view'); + $target->setEntityId($route_match['view_id'] . '.' . $route_match['display_id']); + } + return; } // This case apply with entity canonical routes. $route_parts = explode('.', $route_match['_route']); - if (count($route_parts) === 3 && $route_parts[0] === 'entity' && $route_parts[2] === 'canonical') { + if (count($route_parts) > 1) { $entity_id = ''; $entity = $route_parts[1]; if (isset($route_match[$entity]) && $route_match[$entity] instanceof EntityInterface) { $entity_id = $route_match[$entity]->id(); + $entity = $route_match[$entity]->getEntityTypeId(); } $target->setEntityType($entity); $target->setEntityId((string) $entity_id); @@ -576,6 +609,16 @@ class EntityRender extends Entity { return TRUE; } + if ($target->getEntityType() === 'view') { + $route_view = 'view.' . $target->getEntityId(); + + $anonymous_account = new AnonymousUserSession(); + + // Check access. + return $this->accessManager->checkNamedRoute($route_view, [], $anonymous_account); + + } + try { $storage = $this->entityTypeManager->getStorage($target->getEntityType()); } diff --git a/tests/src/Kernel/EntityMeshViewsTest.php b/tests/src/Kernel/EntityMeshViewsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e9ad2a07557dcfa7248388021b130ae047e0a83e --- /dev/null +++ b/tests/src/Kernel/EntityMeshViewsTest.php @@ -0,0 +1,334 @@ +<?php + +namespace Drupal\Tests\entity_mesh\Kernel; + +use Drupal\Core\Session\AccountInterface; +use Drupal\filter\Entity\FilterFormat; +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; +use Drupal\user\Entity\Role; + +/** + * Tests the Entity Mesh link auditing for taxonomy terms and views. + * + * @group entity_mesh + */ +class EntityMeshViewsTest extends KernelTestBase { + + use ContentTypeCreationTrait; + use UserCreationTrait; + + /** + * Modules to enable. + * + * @var array<string> + */ + protected static $modules = [ + 'system', + 'node', + 'user', + 'field', + 'filter', + 'text', + 'language', + 'entity_mesh', + 'taxonomy', + 'views', + 'path_alias', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Install the necessary schemas. + $this->installEntitySchema('configurable_language'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('path_alias'); + $this->installEntitySchema('taxonomy_term'); + + // Install schemas. + $this->installSchema('entity_mesh', ['entity_mesh']); + $this->installSchema('node', ['node_access']); + + // Install configs but skip entity_mesh to avoid schema validation errors. + $this->installConfig(['filter', 'node', 'system', 'language', 'taxonomy', 'entity_mesh']); + + // Create content type. + $this->createContentType(['type' => 'page', 'name' => 'Page']); + + // Set up language configuration. + $config = $this->config('language.negotiation'); + $config->set('url.prefixes', ['en' => 'en']) + ->save(); + + // Enable the body field in the default view mode. + $this->container->get('entity_display.repository') + ->getViewDisplay('node', 'page', 'full') + ->setComponent('body', [ + // Show label above the body content. + 'label' => 'above', + // Render as basic text. + 'type' => 'text_default', + ]) + ->save(); + + // Create filter format. + $filter_format = FilterFormat::load('basic_html'); + if (!$filter_format) { + $filter_format = FilterFormat::create([ + 'format' => 'basic_html', + 'name' => 'Basic HTML', + 'filters' => [], + ]); + $filter_format->save(); + } + + // Create anonymous role if it doesn't exist. + if (!Role::load(AccountInterface::ANONYMOUS_ROLE)) { + Role::create(['id' => AccountInterface::ANONYMOUS_ROLE, 'label' => 'Anonymous user'])->save(); + } + + // Grant permissions to anonymous users. + $this->grantPermissions(Role::load(AccountInterface::ANONYMOUS_ROLE), [ + 'access content', + ]); + + // Create vocabulary and taxonomy terms. + $this->createTaxonomyTerms(); + + // Set up path aliases for taxonomy terms. + $this->createPathAliases(); + + // Rebuild router and container. + $this->container->get('kernel')->rebuildContainer(); + $this->container->get('router.builder')->rebuild(); + + // Create example nodes with links to taxonomy terms and views. + $this->createExampleNodes(); + } + + /** + * Creates taxonomy terms for testing. + */ + protected function createTaxonomyTerms() { + // Create vocabulary. + Vocabulary::create([ + 'vid' => 'tags', + 'name' => 'Tags', + ])->save(); + + // Create term without alias. + Term::create([ + 'tid' => 1, + 'vid' => 'tags', + 'name' => 'Term without alias', + ])->save(); + + // Create term with alias. + Term::create([ + 'tid' => 2, + 'vid' => 'tags', + 'name' => 'Term with alias', + ])->save(); + + // Create unpublished term without alias. + Term::create([ + 'tid' => 3, + 'vid' => 'tags', + 'name' => 'Unpublished term without alias', + // Set as unpublished. + 'status' => 0, + ])->save(); + } + + /** + * Creates path aliases for taxonomy terms. + */ + protected function createPathAliases() { + // Create alias for the second taxonomy term. + $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([ + 'path' => '/taxonomy/term/2', + 'alias' => '/example-category', + 'langcode' => 'en', + ]); + $path_alias->save(); + } + + /** + * {@inheritdoc} + */ + protected function createExampleNodes() { + // Create a node with links to taxonomy terms and views. + $html_content = ' + <p>Taxonomy term without alias: <a href="/taxonomy/term/1">Term without alias</a></p> + <p>Taxonomy term with alias: <a href="/example-category">Term with alias</a></p> + <p>View with access denied: <a href="/admin/content">Admin content</a></p> + <p>Unpublished taxonomy term without alias: <a href="/taxonomy/term/3">Unpublished term without alias</a></p> + '; + + $node = Node::create([ + 'type' => 'page', + 'title' => 'Test Node with Links', + 'nid' => 1, + 'body' => [ + 'value' => $html_content, + 'format' => 'basic_html', + ], + ]); + $node->save(); + } + + /** + * Fetches records from the 'entity_mesh' table. + */ + protected function fetchEntityMeshRecords() { + $connection = $this->container->get('database'); + $query = $connection->select('entity_mesh', 'em') + ->fields('em', [ + 'id', + 'type', + 'category', + 'subcategory', + 'source_entity_id', + 'source_entity_type', + 'source_entity_bundle', + 'source_entity_langcode', + 'source_title', + 'target_href', + 'target_path', + 'target_scheme', + 'target_link_type', + 'target_entity_type', + 'target_entity_bundle', + 'target_entity_id', + 'target_title', + 'target_entity_langcode', + ]); + $result = $query->execute(); + return $result ? $result->fetchAllAssoc('id', \PDO::FETCH_ASSOC) : []; + } + + /** + * Tests different types of links. + * + * @dataProvider linkCasesProvider + */ + public function testLinks( + $source_entity_id, + $target_href, + $expected_target_link_type, + $excepted_target_subcategory = NULL, + $expected_target_title = NULL, + $expected_target_scheme = NULL, + $expected_target_entity_type = NULL, + $expected_target_entity_bundle = NULL, + $expected_target_entity_id = NULL, + $excepted_target_entity_langcode = NULL, + $excepted_source_entity_type = NULL, + $excepted_source_entity_bundle = NULL, + $excepted_source_entity_langcode = NULL, + $excepted_source_title = NULL, + ) { + // Fetch records from entity_mesh table for assertions. + $records = $this->fetchEntityMeshRecords(); + + // Filter records based on node ID and link type. + $filtered = array_filter($records, function ($record) use ($source_entity_id, $target_href) { + return $record['source_entity_id'] == $source_entity_id && + $record['target_href'] === $target_href; + }); + + // Extract the record by matching 'target_href' with $expected_href. + $record = reset($filtered); + + $this->assertNotEmpty($record, "No record found with target_href: $target_href"); + + $checks = [ + 'target_link_type' => $expected_target_link_type, + 'subcategory' => $excepted_target_subcategory, + 'target_title' => $expected_target_title, + 'target_scheme' => $expected_target_scheme, + 'target_entity_type' => $expected_target_entity_type, + 'target_entity_bundle' => $expected_target_entity_bundle, + 'target_entity_id' => $expected_target_entity_id, + 'target_entity_langcode' => $excepted_target_entity_langcode, + 'source_entity_type' => $excepted_source_entity_type, + 'source_entity_bundle' => $excepted_source_entity_bundle, + 'source_entity_langcode' => $excepted_source_entity_langcode, + 'source_title' => $excepted_source_title, + ]; + + foreach ($checks as $key => $expectedValue) { + if ($expectedValue !== NULL) { + $this->assertEquals($expectedValue, $record[$key]); + } + } + } + + /** + * Provides test cases for different types of links. + */ + public function linkCasesProvider() { + $defaults = [ + 'source_entity_id' => NULL, + 'target_href' => NULL, + 'expected_target_link_type' => NULL, + 'excepted_target_subcategory' => NULL, + 'expected_target_title' => NULL, + 'expected_target_scheme' => NULL, + 'expected_target_entity_type' => NULL, + 'expected_target_entity_bundle' => NULL, + 'expected_target_entity_id' => NULL, + 'excepted_target_entity_langcode' => NULL, + 'excepted_source_entity_type' => NULL, + 'excepted_source_entity_bundle' => NULL, + 'excepted_source_entity_langcode' => NULL, + 'excepted_source_title' => NULL, + ]; + + return [ + 'Taxonomy term without alias' => array_merge($defaults, [ + 'source_entity_id' => 1, + 'target_href' => '/taxonomy/term/1', + 'expected_target_link_type' => 'internal', + 'excepted_target_subcategory' => 'link', + 'expected_target_entity_type' => 'taxonomy_term', + 'expected_target_entity_id' => '1', + ]), + + 'Taxonomy term with alias' => array_merge($defaults, [ + 'source_entity_id' => 1, + 'target_href' => '/example-category', + 'expected_target_link_type' => 'internal', + 'excepted_target_subcategory' => 'link', + 'expected_target_entity_type' => 'taxonomy_term', + 'expected_target_entity_id' => '2', + ]), + + 'View with access denied' => array_merge($defaults, [ + 'source_entity_id' => 1, + 'target_href' => '/admin/content', + 'expected_target_link_type' => 'internal', + 'excepted_target_subcategory' => 'access-denied-link', + ]), + + 'Unpublished taxonomy term without alias' => array_merge($defaults, [ + 'source_entity_id' => 1, + 'target_href' => '/taxonomy/term/3', + 'expected_target_link_type' => 'internal', + 'excepted_target_subcategory' => 'access-denied-link', + 'expected_target_entity_type' => 'taxonomy_term', + 'expected_target_entity_id' => '3', + ]), + ]; + } + +}