From 379309d95ed0d0454408212551e41bc4032d7479 Wed, 15 Feb 2012 03:53:32 +0100
From: hass <hass@85918.no-reply.drupal.org>
Date: Wed, 15 Feb 2012 03:53:17 +0100
Subject: [PATCH] Security diff

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 6ff08f6..9753363 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,7 @@
 linkchecker 7.x-dev
 --------------------------------------
 
+* Fix for access bypass vulnerability.
 * #1438376: D7 Upgrade failed with PHP 5 < 5.3.0, array_replace() missing
 * #1429284: Don't follow redirects
 * #1416830: Link fields are not being extracted
diff --git a/linkchecker.module b/linkchecker.module
index 9c13058..41fe677 100644
--- a/linkchecker.module
+++ b/linkchecker.module
@@ -109,7 +109,7 @@
   );
   // Add the user menu item after node/edit tab.
   $items['user/%user/linkchecker'] = array(
-    'access callback' => '_linkchecker_user_access_own_broken_links_report',
+    'access callback' => '_linkchecker_user_access_account_broken_links_report',
     'access arguments' => array(1),
     'description' => 'Shows a list of broken links in content.',
     'file' => 'linkchecker.pages.inc',
@@ -149,64 +149,217 @@
 /**
  * Access callback for user/%user/linkchecker.
  */
-function _linkchecker_user_access_own_broken_links_report($account) {
+function _linkchecker_user_access_account_broken_links_report($account) {
   global $user;
 
-  // Access to this path is only granted for authenticated users viewing their
-  // own broken links and all administrative users.
-  return $account->uid && ($user->uid == $account->uid || (user_access('administer nodes') && user_access('administer linkchecker'))) && user_access('access own broken links report');
+  // Users with 'access own broken links report' permission can only view their
+  // own report. Users with the 'access broken links report' permission can
+  // view the report for any authenticated user.
+  return $account->uid && (($user->uid == $account->uid && user_access('access own broken links report')) || user_access('access broken links report'));
 }
 
 /**
  * Access callback for linkchecker/%linkchecker_link/edit.
  */
 function _linkchecker_user_access_edit_link_settings($link) {
-  global $user;
+  return user_access('edit link settings') && _linkchecker_link_access($link);
+}
 
-  if (user_access('administer nodes') && user_access('administer linkchecker')) {
-    // Full access to this path is granted to administrative users.
-    return TRUE;
+/**
+* Determines if the current user has access to view a link.
+*
+* Link URLs can contain private information (for example, usernames and
+* passwords). So this module should only display links to a user if the link
+* already appears in at least one place on the site where the user would
+* otherwise have access to see it.
+*/
+function _linkchecker_link_access($link) {
+  $link = (object) $link;
+  return _linkchecker_link_node_ids($link) || _linkchecker_link_comment_ids($link) || _linkchecker_link_block_ids($link);
+}
+
+/**
+* Returns IDs of nodes that contain a link which the current user may be allowed to view.
+*
+* Important note: For performance reasons, this function is not always
+* guaranteed to return the exact list of node IDs that the current user is
+* allowed to view. It will, however, always return an empty array if the user
+* does not have access to view *any* such nodes, thereby meeting the security
+* goals of _linkchecker_link_access() and other places that call it.
+*
+* In the case where a user has access to some of the nodes that contain the
+* link, this function may return some node IDs that the user does not have
+* access to. Therefore, use caution with its results.
+*
+* @param $link
+*   An object representing the link to check.
+* @param $node_author_account
+*   (optional) If a user account object is provided, the returned nodes will
+*   additionally be restricted to only those owned by this account. Otherwise,
+*   nodes owned by any user account may be returned.
+* @return
+*   An array of node IDs that contain the provided link and that the current
+*   user may be allowed to view.
+*/
+function _linkchecker_link_node_ids($link, $node_author_account = NULL) {
+  static $fields_with_node_links = array();
+
+  // If the user cannot access content, there is no need to check further.
+  if (!user_access('access content')) {
+    return array();
+  }
+
+  // Get a list of nodes containing the link, using db_rewrite_sql() to allow
+  // node access modules to exclude nodes that the current user does not have
+  // access to view.
+  if (!empty($node_author_account)) {
+    $nodes = db_select('node', 'n')
+      ->innerJoin('linkchecker_node', 'ln', 'ln.nid = n.nid')
+      ->innerJoin('node_revision', 'r', 'r.vid = n.vid')
+      ->condition('ln.lid', $link->lid)
+      ->condition(db_or()
+        ->condition('n.uid', $node_author_account->uid)
+        ->condition('r.uid', $node_author_account->uid)
+      )
+      ->fields('n', array('nid'))
+      ->execute();
   }
   else {
-    // Verify that $lid is at least in one of the authors nodes or comments.
-    $subquery2 = db_select('node', 'n');
-    $subquery2->innerJoin('node_revision', 'r', 'r.vid = n.vid');
-    $subquery2->innerJoin('linkchecker_node', 'ln', 'ln.nid = n.nid');
-    $subquery2->innerJoin('linkchecker_link', 'll', 'll.lid = ln.lid AND ll.lid = :lid', array(':lid' => $link->lid));
-    $subquery2->condition(db_or()
-      ->condition('n.uid', $user->uid)
-      ->condition('r.uid', $user->uid)
-    );
-    $subquery2->distinct();
-    $subquery2->fields('ll' , array('lid'));
-
-    if (variable_get('linkchecker_scan_comments', 0)) {
-      // Build query for broken links in nodes and comments of the current user.
-      $subquery3 = db_select('comment', 'c');
-      $subquery3->innerJoin('linkchecker_comment', 'lc', 'lc.cid = c.cid');
-      $subquery3->innerJoin('linkchecker_link', 'll', 'll.lid = lc.lid AND ll.lid = :lid', array(':lid' => $link->lid));
-      $subquery3->condition('c.uid', $user->uid);
-      $subquery3->distinct();
-      $subquery3->fields('ll' , array('lid'));
-
-      // UNION the linkchecker_node and linkchecker_comment tables.
-      $subquery1 = db_select($subquery2->union($subquery3), 'q1')->fields('q1', array('lid'));
-    }
-    else {
-      // Build query for broken links in nodes of the current user.
-      $subquery1 = db_select($subquery2, 'q1')->fields('q1', array('lid'));
-    }
-
-    // Build full query.
-    $query = db_select('linkchecker_link', 'll');
-    $query->innerJoin($subquery1, 'q2', 'q2.lid = ll.lid');
-    $query->fields('ll');
-    $is_author_of_lid = $query->countQuery()->execute()->fetchField();
-
-    // This path is only allowed for authenticated users looking at their own
-    // links.
-    return $is_author_of_lid && user_access('edit link settings');
+    $nodes = db_select('node', 'n')
+      ->innerJoin('linkchecker_node', 'ln', 'ln.nid = n.nid')
+      ->condition('ln.lid', $link->lid)
+      ->fields('n', array('nid'))
+      ->execute();
   }
+
+  // Check if the current user has access to view the link in each node.
+  // However, for performance reasons, as soon as we find one node where that
+  // is the case, stop checking and return the remainder of the list.
+  $nids = array();
+  $access_allowed = FALSE;
+  foreach ($nodes as $node) {
+    if ($access_allowed) {
+      $nids[] = $node->nid;
+      continue;
+    }
+    $node = node_load($node->nid);
+    // We must check whether the link is currently part of the node; if not, we
+    // do not want to return it (and it is not safe to, since we cannot know if
+    // it contained access restrictions for the current user at the point which
+    // it was originally extracted by the Link checker module).
+    if (!isset($fields_with_node_links[$node->nid])) {
+      $fields_with_node_links[$node->nid] = _linkchecker_extract_node_links($node, TRUE);
+    }
+    if (empty($fields_with_node_links[$node->nid][$link->url])) {
+      continue;
+    }
+    // If the link only appears in CCK fields and a field access module is
+    // being used, we must check that the current user has access to view at
+    // least one field that contains the link; if they don't, we should not
+    // return the node.
+    $fields = $fields_with_node_links[$node->nid][$link->url];
+    if (!in_array('node', $fields) && module_exists('content') && module_implements('field_access')) {
+      $fields_with_access = array();
+      foreach (content_fields(NULL, $node->type) as $field) {
+        // Only check link and text fields, since those are the only types we
+        // extract links from.
+        if (($field['type'] == 'link' || $field['type'] == 'text') && content_access('view', $field, NULL, $node)) {
+          $fields_with_access[] = $field['field_name'];
+        }
+      }
+      if (!array_intersect($fields, $fields_with_access)) {
+        continue;
+      }
+    }
+    $nids[] = $node->nid;
+    $access_allowed = TRUE;
+  }
+
+  return $nids;
+}
+
+/**
+ * Returns IDs of comments that contain a link which the current user is allowed to view.
+ *
+ * @param $link
+ *   An object representing the link to check.
+ * @param $comment_author_account
+ *   (optional) If a user account object is provided, the returned comments
+ *   will additionally be restricted to only those owned by this account.
+ *   Otherwise, comments owned by any user account may be returned.
+ * @return
+ *   An array of comment IDs that contain the provided link and that the
+ *   current user is allowed to view.
+ */
+function _linkchecker_link_comment_ids($link, $comment_author_account = NULL) {
+  // If the user cannot access comments, there is no need to check further.
+  if (!user_access('access comments')) {
+    return array();
+  }
+
+  // Get a list of comments containing the link, using db_rewrite_sql() to
+  // allow comment access modules to exclude comments that the current user
+  // does not have access to view.
+  if (!empty($comment_author_account)) {
+    $cids = db_select('comment', 'c')
+      ->innerJoin('linkchecker_comment', 'lc', 'lc.cid = c.cid')
+      ->condition('lc.lid', $link->lid)
+      ->condition('c.uid', $comment_author_account->uid)
+      ->fields('c', array('cid'))
+      ->fetchCol()
+      ->execute();
+  }
+  else {
+    $cids = db_select('comment', 'c')
+      ->innerJoin('linkchecker_comment', 'lc', 'lc.cid = c.cid')
+      ->condition('lc.lid', $link->lid)
+      ->fields('c', array('cid'))
+      ->fetchCol()
+      ->execute();
+  }
+
+  // Return the array of comment IDs.
+  return $cids;
+}
+
+/**
+ * Returns IDs of blocks that contain a link which the current user is allowed to view.
+ *
+ * @param $link
+ *   An object representing the link to check.
+ * @return
+ *   An array of custom block IDs that contain the provided link and that the
+ *   current user is allowed to view.
+ */
+function _linkchecker_link_block_ids($link) {
+  global $user;
+
+  // Get the initial list of block IDs.
+  $bids = db_query('SELECT bid FROM {linkchecker_block_custom} WHERE lid = %d', $link->lid)->fetchCol();
+
+  // If the user can administer blocks, they're able to see all block content.
+  if (user_access('administer blocks')) {
+    return $bids;
+  }
+
+  // Otherwise, only return blocks that this user (or anonymous users) have
+  // access to.
+  $rids = array_keys($user->roles);
+  $rids[] = DRUPAL_ANONYMOUS_RID;
+
+  $allowed_bids = db_select('block', 'b')
+    ->leftJoin('block_role', 'r', 'b.module = r.module AND b.delta = r.delta')
+    ->condition('b.module', 'block')
+    ->condition(db_or()
+      ->condition('r.rid', $rids, 'IN')
+      ->condition('r.rid', NULL, 'IS NULL')
+    )
+    ->fields('b', array('delta'))
+    ->distinct()
+    ->fetchCol()
+    ->execute();
+
+  return array_intersect($bids, $allowed_bids);
 }
 
 /**
@@ -511,9 +664,11 @@
   if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'edit') {
     // Show a message on node edit page if a link check failed once or more.
     $ignore_response_codes = preg_split('/(\r\n?|\n)/', variable_get('linkchecker_ignore_response_codes', "200\n206\n302\n304\n401\n403"));
-    $links = db_query('SELECT url, code, fail_count FROM {linkchecker_node} ln INNER JOIN {linkchecker_link} ll ON ln.lid = ll.lid WHERE ln.nid = :nid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':nid' => $node->nid, ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
+    $links = db_query('SELECT ll.* FROM {linkchecker_node} ln INNER JOIN {linkchecker_link} ll ON ln.lid = ll.lid WHERE ln.nid = :nid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':nid' => $node->nid, ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
     foreach ($links as $link) {
-      drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+      if (_linkchecker_link_access($link)) {
+        drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+      }
     }
   }
 }
@@ -617,9 +772,11 @@
         // Show a message on custom block edit page if a link check failed once
         // or more often.
         $ignore_response_codes = preg_split('/(\r\n?|\n)/', variable_get('linkchecker_ignore_response_codes', "200\n206\n302\n304\n401\n403"));
-        $links = db_query('SELECT url, code, fail_count FROM {linkchecker_block_custom} lb INNER JOIN {linkchecker_link} ll ON lb.lid = ll.lid WHERE lb.bid = :bid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':bid' => arg(5), ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
+        $links = db_query('SELECT ll.* FROM {linkchecker_block_custom} lb INNER JOIN {linkchecker_link} ll ON lb.lid = ll.lid WHERE lb.bid = :bid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':bid' => arg(5), ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
         foreach ($links as $link) {
-          drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+          if (_linkchecker_link_access($link)) {
+            drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+          }
         }
       }
 
@@ -639,9 +796,11 @@
         // Show a message on comment edit page if a link check failed once or
         // more often.
         $ignore_response_codes = preg_split('/(\r\n?|\n)/', variable_get('linkchecker_ignore_response_codes', "200\n206\n302\n304\n401\n403"));
-        $links = db_query('SELECT url, code, fail_count FROM {linkchecker_comment} lc INNER JOIN {linkchecker_link} ll ON lc.lid = ll.lid WHERE lc.cid = :cid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':cid' => arg(2), ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
+        $links = db_query('SELECT ll.* FROM {linkchecker_comment} lc INNER JOIN {linkchecker_link} ll ON lc.lid = ll.lid WHERE lc.cid = :cid AND ll.fail_count > :fail_count AND ll.status = :status AND ll.code NOT IN (:codes)', array(':cid' => arg(2), ':fail_count' => 0, ':status' => 1, ':codes' => $ignore_response_codes));
         foreach ($links as $link) {
-          drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+          if (_linkchecker_link_access($link)) {
+            drupal_set_message(format_plural($link->fail_count, 'Link check of <a href="@url">@url</a> failed once (status code: @code).', 'Link check of <a href="@url">@url</a> failed @count times (status code: @code).', array('@url' => $link->url, '@code' => $link->code)), 'warning', FALSE);
+          }
         }
       }
       break;
@@ -671,21 +830,104 @@
 }
 
 /**
- * Add node links to database.
+ * Extracts links from a node.
  *
- * @param object $node
+ * @param $node
  *   The fully populated node object.
- * @param bool $skip_missing_links_detection
- *   To prevent endless batch loops the value need to be TRUE. With FALSE
- *   the need for content re-scans is detected by the number of missing links.
+ * @param $return_field_names
+ *   If set to TRUE, the returned array will contain the link URLs as keys, and
+ *   each element will be an array containing all field names in which the URL
+ *   is found (the special field name "node" is used to represent all scanned
+ *   node content that is not a CCK field). Otherwise, a simple array of URLs
+ *   will be returned.
+ * @return
+ *    An array whose keys are fully qualified and unique URLs found in the node
+ *    (as returned by _linkchecker_extract_links()), or a more complex
+ *    structured array (see above) if $return_field_names is TRUE.
  */
-function _linkchecker_add_node_links($node, $skip_missing_links_detection = FALSE) {
+function _linkchecker_extract_node_links($node, $return_field_names = FALSE) {
   // Get current node language options for url() functions.
   $languages = language_list();
   // Note: An "undefined language" (value: 'und') isn't listed in the available
   // languages variable $languages.
   $url_options = (empty($node->language) || empty($languages[$node->language])) ? array('absolute' => TRUE) : array('language' => $languages[$node->language], 'absolute' => TRUE);
 
+/*
+  // @todo: SECURITY PATCH NOT MIGRATED!!! NON-FUNCTIONAL CODE. CRASHES MODULE!
+
+  // Create array of node fields to scan.
+  $text_items = array();
+  $text_items_by_field = array();
+  $text_items[] = $text_items_by_field['node'][] = _filter_url($node->title, $node->format);
+  $text_items[] = $text_items_by_field['node'][] = _linkchecker_check_markup($node->body, $node->format, FALSE);
+  $text_items[] = $text_items_by_field['node'][] = _linkchecker_check_markup($node->teaser, $node->format, FALSE);
+
+  // Search for links in 'weblink' nodes from 'links' module package.
+  if (module_exists('links_weblink') && $node->type == 'weblink' && !empty($node->links_weblink_url)) {
+    $text_items[] = $text_items_by_field['node'][] = _filter_url(url($node->links_weblink_url, $url_options), $node->format);
+  }
+
+  // Search for links in 'weblinks' nodes from 'weblinks' module.
+  if (module_exists('weblinks') && $node->type == 'weblinks' && !empty($node->url)) {
+    $text_items[] = $text_items_by_field['node'][] = _filter_url(url($node->url, $url_options), $node->format);
+  }
+
+  // Search for CCK-fields of types 'link' and 'text'.
+  if (module_exists('content')) {
+    $fields = content_fields(NULL, $node->type);
+    foreach ($fields as $field) {
+      if (!empty($node->{$field['field_name']})) {
+        if (module_exists('link') && $field['type'] == 'link') {
+          foreach ($node->$field['field_name'] as $delta => $item) {
+            if (!empty($item['url'])) {
+              // Make non-absolute urls absolute or they are not found by _filter_url().
+              $text_items[] = $text_items_by_field[$field['field_name']][] = _filter_url(url($item['url'], $url_options), $node->format);
+            }
+          }
+        }
+        elseif (module_exists('text') && $field['type'] == 'text') {
+          foreach ($node->$field['field_name'] as $delta => $item) {
+            $text_items[] = $text_items_by_field[$field['field_name']][] = _filter_url($item['value'], $node->format);
+          }
+        }
+      }
+    }
+  }
+
+  // Get the absolute node path for extraction of relative links.
+  $path = url('node/'. $node->nid, $url_options);
+
+  // Extract all links in a node.
+  $links = _linkchecker_extract_links(implode(' ', $text_items), $path);
+
+  // Return either the array of links, or an array of field names containing
+  // each link, depending on what was requested.
+  if (!$return_field_names) {
+    return $links;
+  }
+  else {
+    $field_names = array();
+    foreach ($text_items_by_field as $field_name => $items) {
+      foreach ($items as $item) {
+        foreach ($links as $uri => $link) {
+          // We only need to do a quick check here to see if the URL appears
+          // anywhere in the text; if so, that means users with access to this
+          // field will be able to see the URL (and any private data such as
+          // passwords contained in it). This is sufficient for the purposes of
+          // _linkchecker_link_node_ids(), where this information is used.
+          foreach ($link as $original_link) {
+            if (strpos($item, $original_link) !== FALSE) {
+              $field_names[$uri][$field_name] = $field_name;
+            }
+          }
+        }
+      }
+    }
+    return $field_names;
+  }
+*/
+
+  // ALL BELOW IS DEFECT!!!
   $filter = new stdClass;
   $filter->settings['filter_url_length'] = 72;
 
@@ -699,6 +941,45 @@
 
   // Extract all links in a node.
   $links = _linkchecker_extract_links(implode(' ', $text_items), $path);
+
+  // Return either the array of links, or an array of field names containing
+  // each link, depending on what was requested.
+  if (!$return_field_names) {
+    return $links;
+  }
+  else {
+    $field_names = array();
+    foreach ($text_items_by_field as $field_name => $items) {
+      foreach ($items as $item) {
+        foreach ($links as $uri => $link) {
+          // We only need to do a quick check here to see if the URL appears
+          // anywhere in the text; if so, that means users with access to this
+          // field will be able to see the URL (and any private data such as
+          // passwords contained in it). This is sufficient for the purposes of
+          // _linkchecker_link_node_ids(), where this information is used.
+          foreach ($link as $original_link) {
+            if (strpos($item, $original_link) !== FALSE) {
+              $field_names[$uri][$field_name] = $field_name;
+            }
+          }
+        }
+      }
+    }
+    return $field_names;
+  }
+}
+
+/**
+ * Add node links to database.
+ *
+ * @param $node
+ *   The fully populated node object.
+ * @param $skip_missing_links_detection
+ *   To prevent endless batch loops the value need to be TRUE. With FALSE
+ *   the need for content re-scans is detected by the number of missing links.
+ */
+function _linkchecker_add_node_links($node, $skip_missing_links_detection = FALSE) {
+  $links = array_keys(_linkchecker_extract_node_links($node));
 
   // Node have links.
   if (!empty($links)) {
@@ -790,7 +1071,7 @@
   $path = url('node/' . $comment->nid, $url_options);
 
   // Extract all links in a comment.
-  $links = _linkchecker_extract_links(implode(' ', $text_items), $path);
+  $links = array_keys(_linkchecker_extract_links(implode(' ', $text_items), $path));
 
   // Comment have links.
   if (!empty($links)) {
@@ -895,7 +1176,7 @@
   }
 
   // Extract all links in a custom block.
-  $links = _linkchecker_extract_links(implode(' ', $text_items));
+  $links = array_keys(_linkchecker_extract_links(implode(' ', $text_items)));
 
   // Custom block has links.
   if (!empty($links)) {
@@ -1243,7 +1524,9 @@
  *    are not extracted from content, if path is not provided.
  *
  * @return array
- *    Array of full qualified and unique URLs found in content.
+ *    Array whose keys are fully qualified and unique URLs found in the
+ *    content, and whose values are arrays of actual text (raw URLs or paths)
+ *    corresponding to each fully qualified URL.
  */
 function _linkchecker_extract_links($text = '', $content_path = NULL) {
   global $base_root;
@@ -1356,8 +1639,6 @@
     }
   }
 
-  // Decode HTML links into plain text links.
-  $urls = array_map('decode_entities', $urls);
   // Remove empty values.
   $urls = array_filter($urls);
   // Remove duplicate urls.
@@ -1368,16 +1649,19 @@
 
   $links = array();
   foreach ($urls as $url) {
-    // @todo #1149596 HACK - Encode spaces in URLs, so validation equals TRUE
-    // and link gets added.
-    $url_encoded = str_replace(' ', '%20', $url);
+    // Decode HTML links into plain text links.
+    $url_decoded = decode_entities($url);
+
+    // FIXME: #1149596 HACK - Encode spaces in URLs, so validation equals TRUE and link gets added.
+    $url_encoded = str_replace(' ', '%20', $url_decoded);
 
     // Full qualified URLs.
     if ($linkchecker_check_links_types != 2 && valid_url($url_encoded, TRUE)) {
-      $links[] = $url;
+      // Add to Array and change HTML links into plain text links.
+      $links[$url_decoded][] = $url;
     }
     // Skip mailto:, javascript:, etc.
-    elseif (preg_match('/^\w[\w.+]*:/', $url)) {
+    elseif (preg_match('/^\w[\w.+]*:/', $url_decoded)) {
       continue;
     }
     // Local URLs. $linkchecker_check_links_types = 0 or 2
@@ -1386,15 +1670,17 @@
       $absolute_content_path = _linkchecker_absolute_content_path($content_path);
 
       // Absolute local URLs need to start with [/].
-      if (preg_match('!^/!', $url)) {
-        $links[] = $base_root . $url;
+      if (preg_match('!^/!', $url_decoded)) {
+        // Add to Array and change HTML encoded links into plain text links.
+        $links[$base_root . $url_decoded][] = $url;
       }
       // Anchors and URL parameters like "#foo" and "?foo=bar".
-      elseif (!empty($content_path) && preg_match('!^[?#]!', $url)) {
-        $links[] = $content_path . $url;
+      elseif (!empty($content_path) && preg_match('!^[?#]!', $url_decoded)) {
+        // Add to Array and change HTML encoded links into plain text links.
+        $links[$content_path . $url_decoded][] = $url;
       }
       // Relative URLs like "./foo/bar" and "../foo/bar".
-      elseif (!empty($absolute_content_path) && preg_match('!^\.{1,2}/!', $url)) {
+      elseif (!empty($absolute_content_path) && preg_match('!^\.{1,2}/!', $url_decoded)) {
         // Build the URI without hostname before the URI is normalized and
         // dot-segments will be removed. The hostname is added back after the
         // normalization has completed to prevent hostname removal by the regex.
@@ -1402,7 +1688,7 @@
         // RFC 3986, section 5.2.4 to show broken links and over-dot-segmented
         // URIs; e.g. http://example.com/../../foo/bar.
         // For more information, see http://drupal.org/node/832388.
-        $path = substr_replace($absolute_content_path . $url, '', 0, strlen($base_root));
+        $path = substr_replace($absolute_content_path . $url_decoded, '', 0, strlen($base_root));
 
         // Remove './' segments where possible.
         $path = str_replace('/./', '/', $path);
@@ -1416,11 +1702,11 @@
         }
 
         // Glue the hostname and path to full-qualified URI.
-        $links[] = $base_root . $path;
+        $links[$base_root . $path][] = $url;
       }
       // Relative URLs like "test.png".
-      elseif (!empty($absolute_content_path) && preg_match('!^[^/]!', $url)) {
-        $links[] = $absolute_content_path . $url;
+      elseif (!empty($absolute_content_path) && preg_match('!^[^/]!', $url_decoded)) {
+        $links[$absolute_content_path . $url_decoded][] = $url;
       }
       else {
         // @todo Are there more special cases the module need to handle?
@@ -1428,7 +1714,7 @@
     }
   }
 
-  return array_unique($links);
+  return $links;
 }
 
 /**
diff --git a/linkchecker.pages.inc b/linkchecker.pages.inc
index a08c08c..bba79a2 100644
--- a/linkchecker.pages.inc
+++ b/linkchecker.pages.inc
@@ -135,6 +135,21 @@
 
   $rows = array();
   foreach ($result as $link) {
+    // Get the node, block and comment IDs that refer to this broken link and
+    // that the current user has access to.
+    $nids = _linkchecker_link_node_ids($link, $account);
+    $cids = _linkchecker_link_comment_ids($link, $account);
+    $bids = _linkchecker_link_block_ids($link);
+
+    // If the user does not have access to see this link anywhere, do not
+    // display it, for reasons explained in _linkchecker_link_access(). We
+    // still need to fill the table row, though, so as not to throw off the
+    // number of items in the pager.
+    if (empty($nids) && empty($cids) && empty($bids)) {
+      $rows[] = array(array('data' => t('Permission restrictions deny you access to this broken link.'), 'colspan' => count($header)));
+      continue;
+    }
+
     $links = array();
 
     // Show links to link settings.
@@ -143,39 +158,21 @@
     }
 
     // Show link to nodes having this broken link.
-    if (!empty($account)) {
-      $nodes = db_query('SELECT ln.nid
-        FROM {linkchecker_node} ln
-        INNER JOIN {node} n ON n.nid = ln.nid
-        INNER JOIN {node_revision} r ON r.vid = n.vid
-        WHERE ln.lid = :lid AND (n.uid = :uid1 OR r.uid = :uid2)', array(':lid' => $link->lid, ':uid1' => $account->uid, ':uid2' => $account->uid));
-    }
-    else {
-      $nodes = db_query('SELECT nid FROM {linkchecker_node} WHERE lid = :lid', array(':lid' => $link->lid));
-    }
-    foreach ($nodes as $node) {
-      $links[] = l(t('Edit node @node', array('@node' => $node->nid)), 'node/' . $node->nid . '/edit', array('query' => drupal_get_destination()));
+    foreach ($nids as $nid) {
+      $links[] = l(t('Edit node @node', array('@node' => $nid)), 'node/' . $nid . '/edit', array('query' => drupal_get_destination()));
     }
 
     // Show link to comments having this broken link.
-    if (!empty($account) && variable_get('linkchecker_scan_comments', 0)) {
-      $comments = db_query('SELECT lc.cid
-        FROM {linkchecker_comment} lc
-        INNER JOIN {comment} c ON c.cid = lc.cid
-        WHERE lc.lid = :lid AND c.uid = :uid', array(':lid' => $link->lid, ':uid' => $account->uid));
-    }
-    else {
-      $comments = db_query('SELECT cid FROM {linkchecker_comment} WHERE lid = :lid', array(':lid' => $link->lid));
-    }
-    foreach ($comments as $comment) {
-      $links[] = l(t('Edit comment @comment', array('@comment' => $comment->cid)), 'comment/' . $comment->cid . '/edit', array('query' => drupal_get_destination()));
+    if (module_exists('comment') && variable_get('linkchecker_scan_comments', 0)) {
+      foreach ($cids as $cid) {
+        $links[] = l(t('Edit comment @comment', array('@comment' => $cid)), 'comment/' . $cid . '/edit', array('query' => drupal_get_destination()));
+      }
     }
 
     // Show link to blocks having this broken link.
     if ($access_administer_blocks) {
-      $blocks_custom = db_query('SELECT bid FROM {linkchecker_block_custom} WHERE lid = :lid', array(':lid' => $link->lid));
-      foreach ($blocks_custom as $block_custom) {
-        $links[] = l(t('Edit block @block', array('@block' => $block_custom->bid)), 'admin/structure/block/manage/block/' . $block_custom->bid . '/configure', array('query' => drupal_get_destination()));
+      foreach ($bids as $bid) {
+        $links[] = l(t('Edit block @block', array('@block' => $bid)), 'admin/structure/block/manage/block/' . $bid . '/configure', array('query' => drupal_get_destination()));
       }
     }
 
