diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 46a6e31..62a60f9 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -93,6 +93,35 @@ class SearchApiViewsQuery extends views_plugin_query { } /** + * Defines the options used by this query plugin. + * + * Adds an option to bypass access checks. + */ + public function option_definition() { + return parent::option_definition() + array( + 'search_api_bypass_access' => array( + 'default' => FALSE, + ), + ); + } + + /** + * Add settings for the UI. + * + * Adds an option for bypassing access checks. + */ + public function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['search_api_bypass_access'] = array( + '#type' => 'checkbox', + '#title' => t('Bypass access checks'), + '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'), + '#default_value' => $this->options['search_api_bypass_access'], + ); + } + + /** * Builds the necessary info to execute the query. */ public function build(&$view) { @@ -100,6 +129,11 @@ class SearchApiViewsQuery extends views_plugin_query { // Let the pager modify the query to add limits. $this->pager->query(); + + // Add the "search_api_bypass_access" option to the query, if desired. + if (!empty($this->options['search_api_bypass_access'])) { + $this->query->setOption('search_api_bypass_access', TRUE); + } } /** diff --git a/includes/callback_node_access.inc b/includes/callback_node_access.inc new file mode 100644 index 0000000..937256f --- /dev/null +++ b/includes/callback_node_access.inc @@ -0,0 +1,93 @@ +item_type === 'node'; + } + + /** + * Declare the properties that are (or can be) added to items with this callback. + * + * Adds the "search_api_access_node" property. + * + * @see hook_entity_property_info() + * + * @return array + * Information about all additional properties, as specified by + * hook_entity_property_info() (only the inner "properties" array). + */ + public function propertyInfo() { + return array( + 'search_api_access_node' => array( + 'label' => t('Node access information'), + 'description' => t('Data needed to apply node access.'), + 'type' => 'list', + ), + ); + } + + /** + * Alter items before indexing. + * + * Items which are removed from the array won't be indexed, but will be marked + * as clean for future indexing. This could for instance be used to implement + * some sort of access filter for security purposes (e.g., don't index + * unpublished nodes or comments). + * + * @param array $items + * An array of items to be altered, keyed by item IDs. + */ + public function alterItems(array &$items) { + static $account; + + if (!isset($account)) { + // Load the anonymous user. + $account = drupal_anonymous_user(); + } + + if ($this->index->item_type == 'node') { + // Load all notes as once. + foreach ($items as $nid => &$item) { + // Check whether all users have access to the node. + if (!node_access('view', $item, $account)) { + // Get node access grants. + $result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $item->nid)); + + // Store all grants together with it's realms in the item. + foreach ($result as $grant) { + if (!isset($items[$nid]->search_api_access_node)) { + $items[$nid]->search_api_access_node = array(); + } + $items[$nid]->search_api_access_node[] = "node_access_$grant->realm:$grant->gid"; + } + } + else { + // Add the generic view grant if we are not using node access or the + // node is viewable by anonymous users. + $items[$nid]->search_api_access_node = array('node_access__all'); + } + } + } + } + +} diff --git a/includes/query.inc b/includes/query.inc index 958f303..1f9c9a0 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -33,7 +33,12 @@ interface SearchApiQueryInterface { * implementation to use. * - 'search id': A string that will be used as the identifier when storing * this search in the Search API's static cache. - * All options are optional. + * - search_api_access_account: The account which will be used for entity + * access checks, if available and enabled for the index. + * - search_api_bypass_access: If set to TRUE, entity access checks will be + * skipped, even if enabled for the index. + * All options are optional. Third-party modules might define and use other + * options not listed here. * * @throws SearchApiException * If a search on that index (or with those options) won't be possible. @@ -356,6 +361,10 @@ class SearchApiQuery implements SearchApiQueryInterface { * implementation to use. * - 'search id': A string that will be used as the identifier when storing * this search in the Search API's static cache. + * - search_api_access_account: The account which will be used for entity + * access checks, if available and enabled for the index. + * - search_api_bypass_access: If set to TRUE, entity access checks will be + * skipped, even if enabled for the index. * All options are optional. * * @throws SearchApiException diff --git a/search_api.admin.inc b/search_api.admin.inc index 0add170..2cf109b 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1395,7 +1395,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) $type = "list<$type>"; } } - $options['fields'][$key] = array( + $index->options['fields'][$key] = array( 'name' => $field['label'], 'type' => $type, 'boost' => '1.0', diff --git a/search_api.info b/search_api.info index e08bf39..f5885cb 100644 --- a/search_api.info +++ b/search_api.info @@ -13,6 +13,7 @@ files[] = includes/callback_add_url.inc files[] = includes/callback_add_viewed_entity.inc files[] = includes/callback_bundle_filter.inc files[] = includes/callback_language_control.inc +files[] = includes/callback_node_access.inc files[] = includes/datasource.inc files[] = includes/datasource_entity.inc files[] = includes/datasource_external.inc diff --git a/search_api.module b/search_api.module index e1df61b..9c435d1 100644 --- a/search_api.module +++ b/search_api.module @@ -736,6 +736,11 @@ function search_api_search_api_alter_callback_info() { 'description' => t('Lets you determine the language of items in the index.'), 'class' => 'SearchApiAlterLanguageControl', ); + $callbacks['search_api_alter_node_access'] = array( + 'name' => t('Node access'), + 'description' => t('Add node access information to the index.'), + 'class' => 'SearchApiAlterNodeAccess', + ); return $callbacks; } @@ -1213,6 +1218,79 @@ function search_api_get_processors() { } /** + * Implements hook_search_api_query_alter(). + * + * Adds node access to the query, if enabled. + * + * @param SearchApiQueryInterface $query + * The SearchApiQueryInterface object representing the search query. + */ +function search_api_search_api_query_alter(SearchApiQueryInterface $query) { + $index = $query->getIndex(); + // Only add node access if the "search_api_access_node" field is indexed for + // the index, and unless disabled explicitly by the query. + if (!empty($index->options['fields']['search_api_access_node']['indexed']) && !$query->getOption('search_api_bypass_access')) { + $account = $query->getOption('search_api_access_account', $GLOBALS['user']); + if (is_numeric($account)) { + $account = user_load($account); + } + if (is_object($account)) { + try { + _search_api_query_add_node_access($account, $query); + } + catch (SearchApiException $e) { + watchdog('search_api', 'An error occurred while altering node access to a search query: @message.', array('@message' => $e->getMessage()), WATCHDOG_WARNING); + } + } + else { + watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $GLOBALS['user'])), WATCHDOG_WARNING); + } + } +} + +/** + * Build a node access subquery. + * + * @param $account + * The user object, who searches. + * + * @return SearchApiQueryFilter + */ +function _search_api_query_add_node_access($account, SearchApiQueryInterface $query) { + if (!user_access('access content', $account)) { + // Simple hack for returning no results. + $query->condition('status', 0); + $query->condition('status', 1); + watchdog('search_api', 'User !name tried to execute a search, but cannot access content.', array('!name' => theme('username', array('account' => $account))), WATCHDOG_); + return; + } + + // Only filter for user which don't have full node access. + if (!user_access('bypass node access', $account)) { + // Filter by node "published" status. + if (user_access('view own unpublished content')) { + $filter = $query->createFilter('OR'); + $filter->condition('status', NODE_PUBLISHED); + $filter->condition('uid', $account->uid); + $query->filter($filter); + } + else { + $query->condition('status', NODE_PUBLISHED); + } + // Filter by node access grants. + $filter = $query->createFilter('OR'); + $grants = node_access_grants('view', $account); + foreach ($grants as $realm => $gids) { + foreach ($gids as $gid) { + $filter->condition('search_api_access_node', "node_access_$realm:$gid"); + } + } + $filter->condition('search_api_access_node', 'node_access__all'); + $query->filter($filter); + } +} + +/** * Utility function for determining whether a field of the given type contains * text data. *