diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php index 4633508..bc2d8c0 100644 --- a/core/modules/node/lib/Drupal/node/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php @@ -131,6 +131,10 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $ if ($update) { node_reindex_node_search($this->id()); } + + // For either a new node or an updated node, update the node links. + $queue = \Drupal::queue('node_links'); + $queue->createItem($this->id()); } /** @@ -139,12 +143,18 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $ public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) { parent::preDelete($storage_controller, $entities); - // Assure that all nodes deleted are removed from the search index. + // Ensure that all nodes deleted are removed from the search index. if (\Drupal::moduleHandler()->moduleExists('search')) { foreach ($entities as $entity) { search_reindex($entity->nid->value, 'node_search'); } } + + // Remove the node links. + foreach ($entities as $entity) { + $queue = \Drupal::queue('node_links'); + $queue->createItem($entity->nid->value); + } } /** diff --git a/core/modules/node/node.install b/core/modules/node/node.install index b240ecc..f1fa472 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -416,6 +416,38 @@ function node_schema() { ), ); + $schema['node_links'] = array( + 'description' => 'Stores links from one node to another node', + 'fields' => array( + 'source_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid of the node containing the link.', + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this version.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'link_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid that this node links to.', + ), + ), + 'primary key' => array('source_nid', 'langcode', 'link_nid'), + 'indexes' => array( + 'source_nid' => array('source_nid', 'langcode'), + 'link_nid' => array('link_nid'), + ), + ); + return $schema; } @@ -1153,7 +1185,7 @@ function node_update_8020() { } /** - * Converts node_admin_theme variable to config. + * Convert node_admin_theme variable to config. * * @ingroup config_upgrade */ @@ -1164,6 +1196,43 @@ function node_update_8021() { } /** + * Add the {node_links} table. + */ +function node_update_8022() { + db_create_table('node_links', array( + 'description' => 'Stores links from one node to another node', + 'fields' => array( + 'source_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid of the node containing the link.', + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this version.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'link_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid that this node links to.', + ), + ), + 'primary key' => array('source_nid', 'langcode', 'link_nid'), + 'indexes' => array( + 'source_nid' => array('source_nid', 'langcode'), + 'link_nid' => array('link_nid'), + ), + ); +} + +/** * @} End of "addtogroup updates-7.x-to-8.x" * The next series of updates should start at 9000. */ diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 9a4f809..85eb9b4 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -2145,3 +2145,92 @@ function node_comment_unpublish($comment) { node_reindex_node_search($comment->entity_id->value); } } + +/** + * Implements hook_queue_info(). + */ +function node_queue_info() { + $queues['node_links'] = array( + 'title' => t('Update node links'), + 'worker callback' => 'node_update_node_links', + 'cron' => array('time' => 15), + ); + + return $queues; +} + +/** + * Updates node-to-node links in the {node_links} table. + * + * @param int $nid + * The ID of the node whose links need to be updated. + * + * @see node_queue_info() + */ +function node_update_node_links($nid) { + // Delete any existing node links. + db_delete('node_links') + ->condition('source_nid', $nid) + ->execute(); + + // Load the node. + $manager = \Drupal::service('entity.manager'); + $renderer = $manager->getViewBuilder('node'); + $base_url = \Drupal::url('', array(), array('absolute' => TRUE)); + $href_regexp = '@(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'"#>]+)@i'; + $match = array(); + + $node = $manager->getStorageController('node')->load($nid); + $links_found = array(); + if (!$node) { + return; + } + + $languages = $node->getTranslationLanguages(); + + // Go through all of the languages this node has. + foreach ($languages as $language) { + // Render the node in Search Index view mode. + $build = $renderer->view($node, 'search_index', $language->id); + unset($build['#theme']); + $text = drupal_render($build); + + // Scan the text for links to other nodes. + $dom = new \DOMDocument; + $dom->strictErrorChecking = FALSE; + if (!$dom->loadHTML($text) || + !($links = $dom->getElementsByTagName('a'))) { + continue; + } + + foreach ($links as $element) { + $href = $element->getAttribute('href'); + + // See if the link is to another node. + if (preg_match($href_regexp, $href, $match)) { + $path = Drupal::service('path.alias_manager')->getSystemPath($match[1]); + if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) { + $link_nid = $match[1]; + if ($link_nid > 0 && $link_nid != $nid) { + // Record that we found a link. We only want to store one + // link to this node, and only if it's not a self-link. + $links_found[$link_nid][$language->id] = 1; + } + } + } + } + } + + // Save the found links in the database. + foreach ($links_found as $link_nid => $info) { + foreach ($info as $langcode => $dummy) { + $query = db_insert('node_links') + ->fields(array( + 'source_nid' => $nid, + 'langcode' => $langcode, + 'link_nid' => $link_nid, + )) + ->execute(); + } + } +} diff --git a/core/modules/node/node.views.inc b/core/modules/node/node.views.inc index 6c82604..f1f6f11 100644 --- a/core/modules/node/node.views.inc +++ b/core/modules/node/node.views.inc @@ -629,6 +629,52 @@ function node_views_data() { ), ); + // Node links: backlinks. + $data['node_links_to']['table']['group'] = t('Content'); + $data['node_links_to']['table']['join'] = array( + 'node' => array( + 'left_field' => 'nid', + 'field' => 'link_nid', + 'table' => 'node_links', + ), + ); + + $data['node_links_to']['source_nid'] = array( + 'title' => t('Node linking here'), + 'help' => t('Node ID of a node that links to this node'), + 'argument' => array( + 'id' => 'node_nid', + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + 'filter' => array( + 'id' => 'numeric', + ), + ); + + // Node links: outward links. + $data['node_links_from']['table']['group'] = t('Content'); + $data['node_links_from']['table']['join'] = array( + 'node' => array( + 'left_field' => 'nid', + 'field' => 'source_nid', + 'table' => 'node_links', + ), + ); + + $data['node_links_from']['link_nid'] = array( + 'title' => t('Node linked to'), + 'help' => t('Node ID of a node this links to'), + 'argument' => array( + 'id' => 'node_nid', + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + 'filter' => array( + 'id' => 'numeric', + ), + ); + return $data; }