From 8d7d9de4e0f17eb4b2d1f01fcac10d59ff2fbad8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 14 Aug 2013 12:39:32 +0200 Subject: [PATCH 001/278] Issue #2053171 by drunken monkey: Improved tests. --- CHANGELOG.txt | 1 + search_api.install | 9 ++- search_api.test | 196 ++++++++++++++++++++++++--------------------- 3 files changed, 114 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e7f8dc89..193d20d4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2053171 by drunken monkey: Improved tests. - #1433720 by davidwbarratt, drunken monkey, JvE: Fixed handling of empty selection for checkboxes. - #1414078 by drunken monkey, jaxxed: Fixed revert of exportables. diff --git a/search_api.install b/search_api.install index 09e8bb58..c986e2bc 100644 --- a/search_api.install +++ b/search_api.install @@ -322,8 +322,13 @@ function search_api_disable() { $types[$index->item_type][] = $index; } foreach ($types as $type => $indexes) { - $controller = search_api_get_datasource_controller($type); - $controller->stopTracking($indexes); + try { + $controller = search_api_get_datasource_controller($type); + $controller->stopTracking($indexes); + } + catch (SearchApiException $e) { + // Modules defining entity or item types might have been disabled. Ignore. + } } } diff --git a/search_api.test b/search_api.test index 9157397a..1748627c 100644 --- a/search_api.test +++ b/search_api.test @@ -14,13 +14,13 @@ class SearchApiWebTest extends DrupalWebTestCase { protected function drupalGet($path, array $options = array(), array $headers = array()) { $ret = parent::drupalGet($path, $options, $headers); - $this->assertResponse(200, t('HTTP code 200 returned.')); + $this->assertResponse(200, 'HTTP code 200 returned.'); return $ret; } protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { $ret = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post); - $this->assertResponse(200, t('HTTP code 200 returned.')); + $this->assertResponse(200, 'HTTP code 200 returned.'); return $ret; } @@ -54,6 +54,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->clearIndex(); $this->searchNoResults(); $this->deleteServer(); + $this->disableModules(); } protected function deleteDefaultIndex() { @@ -93,7 +94,7 @@ class SearchApiWebTest extends DrupalWebTestCase { 'type' => 'Page', )); $count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() - $count; - $this->assertEqual($count, 5, t('@count items inserted.', array('@count' => $count))); + $this->assertEqual($count, 5, '5 items successfully inserted.'); } protected function insertItem($values) { @@ -104,7 +105,7 @@ class SearchApiWebTest extends DrupalWebTestCase { // This test fails for no apparent reason for drupal.org test bots. // Commenting them out for now. //$this->drupalGet('admin/config/search/search_api'); - //$this->assertText(t('There are no search servers or indexes defined yet.'), t('"No servers" message is displayed.')); + //$this->assertText(t('There are no search servers or indexes defined yet.'), '"No servers" message is displayed.'); } protected function createIndex() { @@ -132,23 +133,23 @@ class SearchApiWebTest extends DrupalWebTestCase { ); $this->drupalPost(NULL, $values, t('Create index')); - $this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), t('The index was successfully created.')); + $this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), 'The index was successfully created.'); $found = strpos($this->getUrl(), 'admin/config/search/search_api/index/' . $id) !== FALSE; - $this->assertTrue($found, t('Correct redirect.')); + $this->assertTrue($found, 'Correct redirect.'); $index = search_api_index_load($id, TRUE); - $this->assertEqual($index->name, $values['name'], t('Name correctly inserted.')); - $this->assertEqual($index->item_type, $values['item_type'], t('Index item type correctly inserted.')); - $this->assertFalse($index->enabled, t('Status correctly inserted.')); - $this->assertEqual($index->description, $values['description'], t('Description correctly inserted.')); - $this->assertNull($index->server, t('Index server correctly inserted.')); - $this->assertEqual($index->options['cron_limit'], $values['options[cron_limit]'], t('Cron batch size correctly inserted.')); + $this->assertEqual($index->name, $values['name'], 'Name correctly inserted.'); + $this->assertEqual($index->item_type, $values['item_type'], 'Index item type correctly inserted.'); + $this->assertFalse($index->enabled, 'Status correctly inserted.'); + $this->assertEqual($index->description, $values['description'], 'Description correctly inserted.'); + $this->assertNull($index->server, 'Index server correctly inserted.'); + $this->assertEqual($index->options['cron_limit'], $values['options[cron_limit]'], 'Cron batch size correctly inserted.'); $values = array( 'additional[field]' => 'parent', ); $this->drupalPost("admin/config/search/search_api/index/$id/fields", $values, t('Add fields')); - $this->assertText(t('The available fields were successfully changed.'), t('Successfully added fields.')); - $this->assertText('Parent » ID', t('!field displayed.', array('!field' => t('Added fields are')))); + $this->assertText(t('The available fields were successfully changed.'), 'Successfully added fields.'); + $this->assertText('Parent » ID', 'Added fields are displayed.'); $values = array( 'fields[id][type]' => 'integer', @@ -177,7 +178,7 @@ class SearchApiWebTest extends DrupalWebTestCase { 'fields[parent:type][indexed]' => 1, ); $this->drupalPost(NULL, $values, t('Save changes')); - $this->assertText(t('The indexed fields were successfully changed. The index was cleared and will have to be re-indexed with the new settings.'), t('Field settings saved.')); + $this->assertText(t('The indexed fields were successfully changed. The index was cleared and will have to be re-indexed with the new settings.'), 'Field settings saved.'); $values = array( 'callbacks[search_api_alter_add_url][status]' => 1, @@ -209,16 +210,16 @@ class SearchApiWebTest extends DrupalWebTestCase { 'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:body]' => 1, ); $this->drupalPost(NULL, $values, t('Save configuration')); - $this->assertText(t("The search index' workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), t('Workflow successfully edited.')); + $this->assertText(t("The search index' workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.'); $this->drupalGet("admin/config/search/search_api/index/$id"); - $this->assertTitle('Search API test index | Drupal', t('Correct title when viewing index.')); - $this->assertText('An index used for testing.', t('!field displayed.', array('!field' => t('Description')))); - $this->assertText('Search API test entity', t('!field displayed.', array('!field' => t('Item type')))); - $this->assertText(format_plural(1, '1 item per cron batch.', '@count items per cron batch.'), t('!field displayed.', array('!field' => t('Cron batch size')))); + $this->assertTitle('Search API test index | Drupal', 'Correct title when viewing index.'); + $this->assertText('An index used for testing.', 'Description displayed.'); + $this->assertText('Search API test entity', 'Item type displayed.'); + $this->assertText(format_plural(1, '1 item per cron batch.', '@count items per cron batch.'), 'Cron batch size displayed.'); $this->drupalGet("admin/config/search/search_api/index/$id/status"); - $this->assertText(t('The index is currently disabled.'), t('"Disabled" status displayed.')); + $this->assertText(t('The index is currently disabled.'), '"Disabled" status displayed.'); } protected function createServer() { @@ -249,25 +250,25 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The server was successfully created.')); $found = strpos($this->getUrl(), 'admin/config/search/search_api/server/' . $id) !== FALSE; - $this->assertTrue($found, t('Correct redirect.')); + $this->assertTrue($found, 'Correct redirect.'); $server = search_api_server_load($id, TRUE); - $this->assertEqual($server->name, $values['name'], t('Name correctly inserted.')); - $this->assertTrue($server->enabled, t('Status correctly inserted.')); - $this->assertEqual($server->description, $values['description'], t('Description correctly inserted.')); - $this->assertEqual($server->class, $values['class'], t('Service class correctly inserted.')); - $this->assertEqual($server->options['test'], $values2['options[form][test]'], t('Service options correctly inserted.')); - $this->assertTitle('Search API test server | Drupal', t('Correct title when viewing server.')); - $this->assertText('A server used for testing.', t('!field displayed.', array('!field' => t('Description')))); - $this->assertText('search_api_test_service', t('!field displayed.', array('!field' => t('Service name')))); - $this->assertText('search_api_test_service description', t('!field displayed.', array('!field' => t('Service description')))); - $this->assertText('search_api_test foo bar', t('!field displayed.', array('!field' => t('Service options')))); + $this->assertEqual($server->name, $values['name'], 'Name correctly inserted.'); + $this->assertTrue($server->enabled, 'Status correctly inserted.'); + $this->assertEqual($server->description, $values['description'], 'Description correctly inserted.'); + $this->assertEqual($server->class, $values['class'], 'Service class correctly inserted.'); + $this->assertEqual($server->options['test'], $values2['options[form][test]'], 'Service options correctly inserted.'); + $this->assertTitle('Search API test server | Drupal', 'Correct title when viewing server.'); + $this->assertText('A server used for testing.', 'Description displayed.'); + $this->assertText('search_api_test_service', 'Service name displayed.'); + $this->assertText('search_api_test_service description', 'Service description displayed.'); + $this->assertText('search_api_test foo bar', 'Service options displayed.'); } protected function checkOverview2() { $this->drupalGet('admin/config/search/search_api'); - $this->assertText('Search API test server', t('!field displayed.', array('!field' => t('Server')))); - $this->assertText('Search API test index', t('!field displayed.', array('!field' => t('Index')))); - $this->assertNoText(t('There are no search servers or indexes defined yet.'), t('"No servers" message not displayed.')); + $this->assertText('Search API test server', 'Server displayed.'); + $this->assertText('Search API test index', 'Index displayed.'); + $this->assertNoText(t('There are no search servers or indexes defined yet.'), '"No servers" message not displayed.'); } protected function enableIndex() { @@ -276,7 +277,7 @@ class SearchApiWebTest extends DrupalWebTestCase { ); $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $values, t('Save settings')); $this->assertText(t('The search index was successfully edited.')); - $this->assertText('Search API test server', t('!field displayed.', array('!field' => t('Server')))); + $this->assertText('Search API test server', 'Server displayed.'); $this->clickLink(t('enable')); $this->assertText(t('The index was successfully enabled.')); @@ -284,17 +285,17 @@ class SearchApiWebTest extends DrupalWebTestCase { protected function searchNoResults() { $this->drupalGet('search_api_test/query/' . $this->index_id); - $this->assertText('result count = 0', t('No search results returned without indexing.')); - $this->assertText('results = ()', t('No search results returned without indexing.')); + $this->assertText('result count = 0', 'No search results returned without indexing.'); + $this->assertText('results = ()', 'No search results returned without indexing.'); } protected function indexItems() { $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status"); - $this->assertText(t('The index is currently enabled.'), t('"Enabled" status displayed.')); - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status')))); - $this->assertText(t('Index now'), t('"Index now" button found.')); - $this->assertText(t('Clear index'), t('"Clear index" button found.')); - $this->assertNoText(t('Re-index content'), t('"Re-index" button not found.')); + $this->assertText(t('The index is currently enabled.'), '"Enabled" status displayed.'); + $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.'); + $this->assertText(t('Index now'), '"Index now" button found.'); + $this->assertText(t('Clear index'), '"Clear index" button found.'); + $this->assertNoText(t('Re-index content'), '"Re-index" button not found.'); // Here we test the indexing + the warning message when some items // can not be indexed. @@ -305,10 +306,10 @@ class SearchApiWebTest extends DrupalWebTestCase { ); $this->drupalPost(NULL, $values, t('Index now')); $this->assertText(t('Successfully indexed @count items.', array('@count' => 7))); - $this->assertText(t('1 item could not be indexed. Check the logs for details.'), t('Index errors warning is displayed.')); - $this->assertNoText(t("Couldn't index items. Check the logs for details."), t("Index error isn't displayed.")); - $this->assertText(t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => 7, '@total' => 10, '@percentage' => 70)), t('!field displayed.', array('!field' => t('Correct index status')))); - $this->assertText(t('Re-indexing'), t('"Re-index" button found.')); + $this->assertText(t('1 item could not be indexed. Check the logs for details.'), 'Index errors warning is displayed.'); + $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); + $this->assertText(t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => 7, '@total' => 10, '@percentage' => 70)), 'Correct index status displayed.'); + $this->assertText(t('Re-indexing'), '"Re-index" button found.'); // Here we're testing the error message when no item could be indexed. // The item with ID 8 is still not indexed. @@ -316,9 +317,9 @@ class SearchApiWebTest extends DrupalWebTestCase { 'limit' => 1, ); $this->drupalPost(NULL, $values, t('Index now')); - $this->assertNoPattern('/' . str_replace('144', '-?\d*', t('Successfully indexed @count items.', array('@count' => 144))) . '/', t('No items could be indexed.')); - $this->assertNoText(t('1 item could not be indexed. Check the logs for details.'), t("Index errors warning isn't displayed.")); - $this->assertText(t("Couldn't index items. Check the logs for details."), t('Index error is displayed.')); + $this->assertNoPattern('/' . str_replace('144', '-?\d*', t('Successfully indexed @count items.', array('@count' => 144))) . '/', 'No items could be indexed.'); + $this->assertNoText(t('1 item could not be indexed. Check the logs for details.'), "Index errors warning isn't displayed."); + $this->assertText(t("Couldn't index items. Check the logs for details."), 'Index error is displayed.'); // Here we test the indexing of all the remaining items. variable_set('search_api_test_index_all', TRUE); @@ -327,20 +328,20 @@ class SearchApiWebTest extends DrupalWebTestCase { ); $this->drupalPost(NULL, $values, t('Index now')); $this->assertText(t('Successfully indexed @count items.', array('@count' => 3))); - $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), t("Index errors warning isn't displayed.")); - $this->assertNoText(t("Couldn't index items. Check the logs for details."), t("Index error isn't displayed.")); - $this->assertText(t('All items have been indexed (@indexed / @total).', array('@indexed' => 10, '@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status')))); - $this->assertNoText(t('Index now'), t('"Index now" button no longer displayed.')); + $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed."); + $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); + $this->assertText(t('All items have been indexed (@indexed / @total).', array('@indexed' => 10, '@total' => 10)), 'Correct index status displayed.'); + $this->assertNoText(t('Index now'), '"Index now" button no longer displayed.'); } protected function searchSuccess() { $this->drupalGet('search_api_test/query/' . $this->index_id); - $this->assertText('result count = 10', t('Correct search result count returned after indexing.')); - $this->assertText('results = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)', t('Correct search results returned after indexing.')); + $this->assertText('result count = 10', 'Correct search result count returned after indexing.'); + $this->assertText('results = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)', 'Correct search results returned after indexing.'); $this->drupalGet('search_api_test/query/' . $this->index_id . '/foo/2/4'); - $this->assertText('result count = 10', t('Correct search result count with ranged query.')); - $this->assertText('results = (3, 4, 5, 6)', t('Correct search results with ranged query.')); + $this->assertText('result count = 10', 'Correct search result count with ranged query.'); + $this->assertText('results = (3, 4, 5, 6)', 'Correct search results with ranged query.'); } protected function editServer() { @@ -351,22 +352,40 @@ class SearchApiWebTest extends DrupalWebTestCase { ); $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $values, t('Save settings')); $this->assertText(t('The search server was successfully edited.')); - $this->assertText('test-name-foo', t('!field changed.', array('!field' => t('Name')))); - $this->assertText('test-description-bar', t('!field changed.', array('!field' => t('Description')))); - $this->assertText('test-test-baz', t('!field changed.', array('!field' => t('Service options')))); + $this->assertText('test-name-foo', 'Name changed.'); + $this->assertText('test-description-bar', 'Description changed.'); + $this->assertText('test-test-baz', 'Service options changed.'); } protected function clearIndex() { $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/status", array(), t('Clear index')); $this->assertText(t('The index was successfully cleared.')); - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status')))); + $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.'); } protected function deleteServer() { $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm')); - $this->assertNoText('test-name-foo', t('Server no longer listed.')); + $this->assertNoText('test-name-foo', 'Server no longer listed.'); $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status"); - $this->assertText(t('The index is currently disabled.'), t('The index was disabled and removed from the server.')); + $this->assertText(t('The index is currently disabled.'), 'The index was disabled and removed from the server.'); + } + + protected function disableModules() { + module_disable(array('search_api_test'), FALSE); + $this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.'); + module_disable(array('search_api'), FALSE); + $this->assertFalse(module_exists('search_api'), 'Search API module was successfully disabled.'); + + drupal_uninstall_modules(array('search_api_test'), FALSE); + $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'Test module was successfully uninstalled.'); + $this->assertFalse(db_table_exists('search_api_test'), 'Test module table was successfully removed.'); + drupal_uninstall_modules(array('search_api'), FALSE); + $this->assertEqual(drupal_get_installed_schema_version('search_api', TRUE), SCHEMA_UNINSTALLED, 'Search API module was successfully uninstalled.'); + $this->assertFalse(db_table_exists('search_api_server'), 'Search server table was successfully removed.'); + $this->assertFalse(db_table_exists('search_api_index'), 'Search index table was successfully removed.'); + $this->assertFalse(db_table_exists('search_api_item'), 'Index items table was successfully removed.'); + $this->assertNull(variable_get('search_api_tasks'), 'Tasks variable was correctly removed.'); + $this->assertNull(variable_get('search_api_index_worker_callback_runtime'), 'Worker runtime variable was correctly removed.'); } } @@ -446,41 +465,38 @@ class SearchApiUnitTest extends DrupalWebTestCase { public function checkQueryParseKeys() { $options['parse mode'] = 'direct'; $mode = &$options['parse mode']; - $num = 1; $query = new SearchApiQuery($this->index, $options); $modes = $query->parseModes(); $query->keys('foo'); - $this->assertEqual($query->getKeys(), 'foo', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), 'foo', '"Direct query" parse mode, test 1.'); $query->keys('foo bar'); - $this->assertEqual($query->getKeys(), 'foo bar', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), 'foo bar', '"Direct query" parse mode, test 2.'); $query->keys('(foo bar) OR "bar baz"'); - $this->assertEqual($query->getKeys(), '(foo bar) OR "bar baz"', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), '(foo bar) OR "bar baz"', '"Direct query" parse mode, test 3.'); $mode = 'single'; - $num = 1; $query = new SearchApiQuery($this->index, $options); $query->keys('foo'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), '"Single term" parse mode, test 1.'); $query->keys('foo bar'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo bar'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo bar'), '"Single term" parse mode, test 2.'); $query->keys('(foo bar) OR "bar baz"'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', '(foo bar) OR "bar baz"'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', '(foo bar) OR "bar baz"'), '"Single term" parse mode, test 3.'); $mode = 'terms'; - $num = 1; $query = new SearchApiQuery($this->index, $options); $query->keys('foo'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), '"Multiple terms" parse mode, test 1.'); $query->keys('foo bar'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo', 'bar'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo', 'bar'), '"Multiple terms" parse mode, test 2.'); $query->keys('(foo bar) OR "bar baz"'); - $this->assertEqual($query->getKeys(), array('(foo', 'bar)', 'OR', 'bar baz', '#conjunction' => 'AND'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('(foo', 'bar)', 'OR', 'bar baz', '#conjunction' => 'AND'), '"Multiple terms" parse mode, test 3.'); // http://drupal.org/node/1468678 $query->keys('"Münster"'); - $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++))); + $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), '"Multiple terms" parse mode, test 4.'); } public function checkIgnoreCaseProcessor() { @@ -524,30 +540,30 @@ class SearchApiUnitTest extends DrupalWebTestCase { $processor = new SearchApiIgnoreCase($this->index, array('fields' => array('name' => 'name'))); $tmp = $items; $processor->preprocessIndexItems($tmp); - $this->assertEqual($tmp[1]['name']['value'], $processed, t('!type field was processed.', array('!type' => 'name'))); - $this->assertEqual($tmp[1]['mail']['value'], $orig, t("!type field wasn't processed.", array('!type' => 'mail'))); + $this->assertEqual($tmp[1]['name']['value'], $processed, 'Name field was processed.'); + $this->assertEqual($tmp[1]['mail']['value'], $orig, "Mail field wasn't processed."); $query = new SearchApiQuery($this->index); $query->keys('Foo "baR BaZ" fOObAr1'); $query->condition('name', 'FOO'); $query->condition('mail', 'BAR'); $processor->preprocessSearchQuery($query); - $this->assertEqual($query->getKeys(), $keys1, t('Search keys were processed correctly.')); - $this->assertEqual($query->getFilter()->getFilters(), $filters1, t('Filters were processed correctly.')); + $this->assertEqual($query->getKeys(), $keys1, 'Search keys were processed correctly.'); + $this->assertEqual($query->getFilter()->getFilters(), $filters1, 'Filters were processed correctly.'); $processor = new SearchApiIgnoreCase($this->index, array('fields' => array('name' => 'name', 'mail' => 'mail'))); $tmp = $items; $processor->preprocessIndexItems($tmp); - $this->assertEqual($tmp[1]['name']['value'], $processed, t('!type field was processed.', array('!type' => 'name'))); - $this->assertEqual($tmp[1]['mail']['value'], $processed, t('!type field was processed.', array('!type' => 'mail'))); + $this->assertEqual($tmp[1]['name']['value'], $processed, 'Name field was processed.'); + $this->assertEqual($tmp[1]['mail']['value'], $processed, 'Mail field was processed.'); $query = new SearchApiQuery($this->index); $query->keys('Foo "baR BaZ" fOObAr1'); $query->condition('name', 'FOO'); $query->condition('mail', 'BAR'); $processor->preprocessSearchQuery($query); - $this->assertEqual($query->getKeys(), $keys2, t('Search keys were processed correctly.')); - $this->assertEqual($query->getFilter()->getFilters(), $filters2, t('Filters were processed correctly.')); + $this->assertEqual($query->getKeys(), $keys2, 'Search keys were processed correctly.'); + $this->assertEqual($query->getFilter()->getFilters(), $filters2, 'Filters were processed correctly.'); } public function checkTokenizer() { @@ -614,22 +630,22 @@ class SearchApiUnitTest extends DrupalWebTestCase { $processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[^\p{L}\p{N}]', 'ignorable' => '[-]')); $tmp = $items; $processor->preprocessIndexItems($tmp); - $this->assertEqual($tmp[1]['name']['value'], $processed1, t('Value was correctly tokenized with default settings.')); + $this->assertEqual($tmp[1]['name']['value'], $processed1, 'Value was correctly tokenized with default settings.'); $query = new SearchApiQuery($this->index, array('parse mode' => 'direct')); $query->keys("foo \"bar-baz\" \n\t foobar1"); $processor->preprocessSearchQuery($query); - $this->assertEqual($query->getKeys(), 'foo barbaz foobar1', t('Search keys were processed correctly.')); + $this->assertEqual($query->getKeys(), 'foo barbaz foobar1', 'Search keys were processed correctly.'); $processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[-a]', 'ignorable' => '\s')); $tmp = $items; $processor->preprocessIndexItems($tmp); - $this->assertEqual($tmp[1]['name']['value'], $processed2, t('Value was correctly tokenized with custom settings.')); + $this->assertEqual($tmp[1]['name']['value'], $processed2, 'Value was correctly tokenized with custom settings.'); $query = new SearchApiQuery($this->index, array('parse mode' => 'direct')); $query->keys("foo \"bar-baz\" \n\t foobar1"); $processor->preprocessSearchQuery($query); - $this->assertEqual($query->getKeys(), 'foo"b r b z"foob r1', t('Search keys were processed correctly.')); + $this->assertEqual($query->getKeys(), 'foo"b r b z"foob r1', 'Search keys were processed correctly.'); } public function checkHtmlFilter() { @@ -694,7 +710,7 @@ END; $processor->preprocessIndexItems($tmp); $processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[\s.:]', 'ignorable' => '')); $processor->preprocessIndexItems($tmp); - $this->assertEqual($tmp[1]['name']['value'], $processed1, t('Text was correctly processed.')); + $this->assertEqual($tmp[1]['name']['value'], $processed1, 'Text was correctly processed.'); } } From 6401a43183c5ae48cbffa384ddcf6bb75c05430a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 14 Aug 2013 12:40:30 +0200 Subject: [PATCH 002/278] Issue #1878606 by drunken monkey: Fixed labels for boolean facets. --- CHANGELOG.txt | 1 + .../search_api_facetapi.module | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 193d20d4..dad11789 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1878606 by drunken monkey: Fixed labels for boolean facets. - #2053171 by drunken monkey: Improved tests. - #1433720 by davidwbarratt, drunken monkey, JvE: Fixed handling of empty selection for checkboxes. diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index feff6840..64aec586 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -316,25 +316,49 @@ function search_api_facetapi_facet_map_callback(array $values, array $options = /** * Creates a human-readable label for single facet filter values. + * + * @param array $values + * The values for which labels should be returned. + * @param array $options + * An associative array containing the following information about the facet: + * - field: Field information, as stored in the index, but with an additional + * "key" property set to the field's internal name. + * - index id: The machine name of the index for this facet. + * - map callback: (optional) A callback that will be called at the beginning, + * which allows initial mapping of filters. Only values not mapped by that + * callback will be processed by this method. + * - value callback: A callback used to map single values and the limits of + * ranges. The signature is the same as for this function, but all values + * will be single values. + * - missing label: (optional) The label used for the "missing" facet. + * + * @return array + * An array mapping raw facet values to their labels. */ function _search_api_facetapi_facet_create_label(array $values, array $options) { $field = $options['field']; + $map = array(); + $n = count($values); + // For entities, we can simply use the entity labels. if (isset($field['entity_type'])) { $type = $field['entity_type']; $entities = entity_load($type, $values); - $map = array(); foreach ($entities as $id => $entity) { $label = entity_label($type, $entity); if ($label) { $map[$id] = $label; } } - return $map; + if (count($map) == $n) { + return $map; + } } + // Then, we check whether there is an options list for the field. $index = search_api_index_load($options['index id']); $wrapper = $index->entityWrapper(); + $values = drupal_map_assoc($values); foreach (explode(':', $field['key']) as $part) { if (!isset($wrapper->$part)) { $wrapper = NULL; @@ -345,12 +369,18 @@ function _search_api_facetapi_facet_create_label(array $values, array $options) $wrapper = $wrapper[0]; } } - if ($wrapper && ($options = $wrapper->optionsList('view'))) { - return $options; + if ($wrapper && ($options_list = $wrapper->optionsList('view'))) { + // We have no use for empty strings, as then the facet links would be + // invisible. + $map += array_intersect_key(array_filter($options_list, 'strlen'), $values); + if (count($map) == $n) { + return $map; + } } - // As a "last resort" we try to create a label based on the field type. - $map = array(); - foreach ($values as $value) { + + // As a "last resort" we try to create a label based on the field type, for + // all values that haven't got a mapping yet. + foreach (array_diff_key($values, $map) as $value) { switch ($field['type']) { case 'boolean': $map[$value] = $value ? t('true') : t('false'); From 54ed9a8ed07158e91f9445f58db890ac50babfaa Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 14 Aug 2013 12:42:19 +0200 Subject: [PATCH 003/278] Issue #2052701 by drunken monkey, erdos: Fixed cron queue state when disabling the module. --- CHANGELOG.txt | 2 ++ search_api.install | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dad11789..10758690 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2052701 by drunken monkey, erdos: Fixed cron queue state when disabling the + module. - #1878606 by drunken monkey: Fixed labels for boolean facets. - #2053171 by drunken monkey: Improved tests. - #1433720 by davidwbarratt, drunken monkey, JvE: Fixed handling of empty diff --git a/search_api.install b/search_api.install index c986e2bc..8a3366cc 100644 --- a/search_api.install +++ b/search_api.install @@ -330,6 +330,7 @@ function search_api_disable() { // Modules defining entity or item types might have been disabled. Ignore. } } + DrupalQueue::get('search_api_indexing_queue')->deleteQueue(); } /** From a7951862c71839e4d5cce2313210d91b794b424d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 26 Aug 2013 15:20:34 +0200 Subject: [PATCH 004/278] Issue #2057867 by drunken monkey: Fixed multiple values for taxonomy contextual filter. --- CHANGELOG.txt | 2 ++ .../includes/handler_argument.inc | 11 ++++++++++ .../handler_argument_taxonomy_term.inc | 20 +++++++++++++------ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 10758690..d15b4faa 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2057867 by drunken monkey: Fixed multiple values for taxonomy contextual + filter. - #2052701 by drunken monkey, erdos: Fixed cron queue state when disabling the module. - #1878606 by drunken monkey: Fixed labels for boolean facets. diff --git a/contrib/search_api_views/includes/handler_argument.inc b/contrib/search_api_views/includes/handler_argument.inc index 7891e548..f3a97886 100644 --- a/contrib/search_api_views/includes/handler_argument.inc +++ b/contrib/search_api_views/includes/handler_argument.inc @@ -12,6 +12,17 @@ class SearchApiViewsHandlerArgument extends views_handler_argument { */ public $query; + /** + * The operator to use for multiple arguments. + * + * Either "and" or "or". + * + * @var string + * + * @see views_break_phrase + */ + public $operator; + /** * Determine if the argument can generate a breadcrumb * diff --git a/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc b/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc index 56667847..3d9fdfc0 100644 --- a/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc @@ -20,34 +20,42 @@ class SearchApiViewsHandlerArgumentTaxonomyTerm extends SearchApiViewsHandlerArg $this->fillValue(); } + $outer_conjunction = strtoupper($this->operator); + if (empty($this->options['not'])) { $operator = '='; - $conjunction = 'OR'; + $inner_conjunction = 'OR'; } else { $operator = '<>'; - $conjunction = 'AND'; + $inner_conjunction = 'AND'; } if (!empty($this->value)) { $terms = entity_load('taxonomy_term', $this->value); - $vocabularies = taxonomy_vocabulary_get_names(); if (!empty($terms)) { - $filter = $this->query->createFilter($conjunction); + $filter = $this->query->createFilter($outer_conjunction); $vocabulary_fields = $this->definition['vocabulary_fields']; $vocabulary_fields += array('' => array()); foreach ($terms as $term) { + $inner_filter = $filter; + if ($outer_conjunction != $inner_conjunction) { + $inner_filter = $this->query->createFilter($inner_conjunction); + } // Set filters for all term reference fields which don't specify a // vocabulary, as well as for all fields specifying the term's // vocabulary. if (!empty($this->definition['vocabulary_fields'][$term->vocabulary_machine_name])) { foreach ($this->definition['vocabulary_fields'][$term->vocabulary_machine_name] as $field) { - $filter->condition($field, $term->tid, $operator); + $inner_filter->condition($field, $term->tid, $operator); } } foreach ($vocabulary_fields[''] as $field) { - $filter->condition($field, $term->tid, $operator); + $inner_filter->condition($field, $term->tid, $operator); + } + if ($outer_conjunction != $inner_conjunction) { + $filter->filter($inner_filter); } } From a4aa4cb970c13ac76954384338aa9112a9fd0b07 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 26 Aug 2013 15:24:42 +0200 Subject: [PATCH 005/278] Issue #2069023 by drunken monkey: Fixed reaction to disabled modules. --- CHANGELOG.txt | 1 + search_api.api.php | 16 ++--- search_api.module | 150 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 145 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d15b4faa..5d4d36df 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2069023 by drunken monkey: Fixed reaction to disabled modules. - #2057867 by drunken monkey: Fixed multiple values for taxonomy contextual filter. - #2052701 by drunken monkey, erdos: Fixed cron queue state when disabling the diff --git a/search_api.api.php b/search_api.api.php index 22f9acbf..9c8857e9 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -15,8 +15,6 @@ * * Note: The ids should be valid PHP identifiers. * - * @see hook_search_api_service_info_alter() - * * @return array * An associative array of search service classes, keyed by a unique * identifier and containing associative arrays with the following keys: @@ -27,6 +25,8 @@ * the "direct" parse mode and other specific things to keep in mind. * - class: The service class, which has to implement the * SearchApiServiceInterface interface. + * + * @see hook_search_api_service_info_alter() */ function hook_search_api_service_info() { $services['example_some'] = array( @@ -49,13 +49,14 @@ function hook_search_api_service_info() { * Alter the Search API service info. * * Modules may implement this hook to alter the information that defines Search - * API service. All properties that are available in - * hook_search_api_service_info() can be altered here. - * - * @see hook_search_api_service_info() + * API services. All properties that are available in + * hook_search_api_service_info() can be altered here, with the addition of the + * "module" key specifying the module that originally defined the service class. * * @param array $service_info * The Search API service info array, keyed by service id. + * + * @see hook_search_api_service_info() */ function hook_search_api_service_info_alter(array &$service_info) { foreach ($service_info as $id => $info) { @@ -125,7 +126,8 @@ function hook_search_api_item_type_info() { * * Modules may implement this hook to alter the information that defines an * item type. All properties that are available in - * hook_search_api_item_type_info() can be altered here. + * hook_search_api_item_type_info() can be altered here, with the addition of + * the "module" key specifying the module that originally defined the type. * * @param array $infos * The item type info array, keyed by type identifier. diff --git a/search_api.module b/search_api.module index 0c2e0249..c97ac4d9 100644 --- a/search_api.module +++ b/search_api.module @@ -723,6 +723,72 @@ function search_api_features_export_alter(&$export, $module_name) { } } +/** + * Implements hook_system_info_alter(). + * + * Checks if the module provides any search item types or service classes. If it + * does, and there are search indexes using those item types, respectively + * servers using those service classes, the module is set to "required". + * + * Heavily borrowed from field_system_info_alter(). + * + * @see hook_search_api_item_type_info() + */ +function search_api_system_info_alter(&$info, $file, $type) { + if ($type != 'module' || $file->name == 'search_api') { + return; + } + // Check for defined item types. + if (module_hook($file->name, 'search_api_item_type_info')) { + $types = array(); + foreach (search_api_get_item_type_info() as $type => $type_info) { + if ($type_info['module'] == $file->name) { + $types[] = $type; + } + } + if ($types) { + $sql = 'SELECT machine_name, name FROM {search_api_index} WHERE item_type IN (:types)'; + $indexes = db_query($sql, array(':types' => $types))->fetchAllKeyed(); + if ($indexes) { + $info['required'] = TRUE; + + $links = array(); + foreach ($indexes as $id => $name) { + $links[] = l($name, "admin/config/search/search_api/index/$id"); + } + + $args = array('!indexes' => implode(', ', $links)); + $info['explanation'] = format_plural(count($indexes), 'Item type in use by the following index: !indexes.', 'Item type(s) in use by the following indexes: !indexes.', $args); + } + } + } + // Check for defined service classes. + if (module_hook($file->name, 'search_api_service_info')) { + $classes = array(); + foreach (search_api_get_service_info() as $class => $class_info) { + if ($class_info['module'] == $file->name) { + $classes[] = $class; + } + } + if ($classes) { + $sql = 'SELECT machine_name, name FROM {search_api_server} WHERE class IN (:classes)'; + $servers = db_query($sql, array(':classes' => $classes))->fetchAllKeyed(); + if ($servers) { + $info['required'] = TRUE; + + $links = array(); + foreach ($servers as $id => $name) { + $links[] = l($name, "admin/config/search/search_api/server/$id"); + } + + $args = array('!servers' => implode(', ', $links)); + $explanation = format_plural(count($servers), 'Service class in use by the following server: !servers.', 'Service class(es) in use by the following servers: !servers.', $args); + $info['explanation'] = (!empty($info['explanation']) ? $info['explanation'] . ' ' : '') . $explanation; + } + } + } +} + /** * Implements hook_entity_insert(). * @@ -844,18 +910,20 @@ function search_api_search_api_item_type_info() { * Implements hook_modules_enabled(). */ function search_api_modules_enabled(array $modules) { - // New modules might offer additional entity types, invalidating the cached - // item type information. + // New modules might offer additional item types or service classes, + // invalidating the cached information. drupal_static_reset('search_api_get_item_type_info'); + drupal_static_reset('search_api_get_service_info'); } /** * Implements hook_modules_disabled(). */ function search_api_modules_disabled(array $modules) { - // The disabled modules might have offered entity types, which are now - // invalid. Therefore, clear the cached item type informaiton. + // The disabled modules might have offered item types or service classes, + // invalidating the cached information. drupal_static_reset('search_api_get_item_type_info'); + drupal_static_reset('search_api_get_service_info'); } /** @@ -1480,22 +1548,48 @@ function search_api_get_data_type_info($type = NULL) { * * @see hook_search_api_service_info() * - * @param $id + * @param string|null $id * The ID of the service info to retrieve. * * @return array * If $id was not specified, an array of all available service classes. * Otherwise, either the service info with the specified id (if it exists), - * or NULL. + * or NULL. Service class information is formatted as specified by + * hook_search_api_service_info(), with the addition of a "module" key + * specifying the module that adds a certain class. */ function search_api_get_service_info($id = NULL) { $services = &drupal_static(__FUNCTION__); if (!isset($services)) { - $services = module_invoke_all('search_api_service_info'); + // Inlined version of module_invoke_all() to add "module" keys. + $services = array(); + foreach (module_implements('search_api_service_info') as $module) { + $function = $module . '_search_api_service_info'; + if (function_exists($function)) { + $new_services = $function(); + if (isset($new_services) && is_array($new_services)) { + foreach ($new_services as $service => $info) { + $new_services[$service] += array('module' => $module); + } + } + $services += $new_services; + } + } - // Allow other modules to alter definitions - drupal_alter('search_api_service_info', $services); + // Same for drupal_alter(). + foreach (module_implements('search_api_service_info_alter') as $module) { + $function = $module . '_search_api_service_info_alter'; + if (function_exists($function)) { + $old = $services; + $function($services); + if ($new_services = array_diff_key($services, $old)) { + foreach ($new_services as $service => $info) { + $services[$service] += array('module' => $module); + } + } + } + } } if (isset($id)) { @@ -1507,15 +1601,15 @@ function search_api_get_service_info($id = NULL) { /** * Returns information for either all item types, or a specific one. * - * @param $type + * @param string|null $type * If set, the item type whose information should be returned. * - * @return + * @return array|null * If $type is given, either an array containing the information of that item * type, or NULL if it is unknown. Otherwise, an array keyed by type IDs * containing the information for all item types. Item type information is - * formatted as specified by hook_search_api_item_type_info(), and has all - * optional fields filled with the defaults. + * formatted as specified by hook_search_api_item_type_info(), with the + * addition of a "module" key specifying the module that adds a certain type. * * @see hook_search_api_item_type_info() */ @@ -1523,8 +1617,34 @@ function search_api_get_item_type_info($type = NULL) { $types = &drupal_static(__FUNCTION__); if (!isset($types)) { - $types = module_invoke_all('search_api_item_type_info'); - drupal_alter('search_api_item_type_info', $types); + // Inlined version of module_invoke_all() to add "module" keys. + $types = array(); + foreach (module_implements('search_api_item_type_info') as $module) { + $function = $module . '_search_api_item_type_info'; + if (function_exists($function)) { + $new_types = $function(); + if (isset($new_types) && is_array($new_types)) { + foreach ($new_types as $id => $info) { + $new_types[$id] += array('module' => $module); + } + } + $types += $new_types; + } + } + + // Same for drupal_alter(). + foreach (module_implements('search_api_item_type_info_alter') as $module) { + $function = $module . '_search_api_item_type_info_alter'; + if (function_exists($function)) { + $old = $types; + $function($types); + if ($new_types = array_diff_key($types, $old)) { + foreach ($new_types as $id => $info) { + $types[$id] += array('module' => $module); + } + } + } + } } if (isset($type)) { From 2bb581c42da11f9a08277b3c75526d67074a6c7c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 26 Aug 2013 22:03:35 +0200 Subject: [PATCH 006/278] Issue #2071229 by drunken monkey: Fixed use of core search constant. --- CHANGELOG.txt | 1 + includes/processor_highlight.inc | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d4d36df..e8538a5e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2071229 by drunken monkey: Fixed use of core search constant. - #2069023 by drunken monkey: Fixed reaction to disabled modules. - #2057867 by drunken monkey: Fixed multiple values for taxonomy contextual filter. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index f5d463d5..979d4267 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -19,13 +19,28 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { */ protected static $boundary; + /** + * PREG regular expression for splitting words. + * + * We highlight around non-indexable or CJK characters. + * + * @var string + */ + protected static $split; + /** * {@inheritdoc} */ public function __construct(SearchApiIndex $index, array $options = array()) { parent::__construct($index, $options); - self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . PREG_CLASS_CJK . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . PREG_CLASS_CJK . ']))'; + $cjk = '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' . + '\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' . + '\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' . + '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' . + '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}'; + self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']))'; + self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']+/iu'; } /** @@ -212,7 +227,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { return $this->flattenKeysArray($keys); } - $keywords = preg_split('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . PREG_CLASS_CJK . ']+/iu', $keys); + $keywords = preg_split(self::$split, $keys); // Assure there are no duplicates. (This is actually faster than // array_unique() by a factor of 3 to 4.) $keywords = drupal_map_assoc(array_filter($keywords)); From f8c7037069309ca7f0183ba9716f4a73ddabf664 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 29 Aug 2013 18:57:17 +0200 Subject: [PATCH 007/278] Issue #2077035 by maciej.zgadzaj: Fixed whitespace recognition for search keys. --- CHANGELOG.txt | 1 + includes/query.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e8538a5e..41d1154d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2077035 by maciej.zgadzaj: Fixed whitespace recognition for search keys. - #2071229 by drunken monkey: Fixed use of core search constant. - #2069023 by drunken monkey: Fixed reaction to disabled modules. - #2057867 by drunken monkey: Fixed multiple values for taxonomy contextual diff --git a/includes/query.inc b/includes/query.inc index d23ba361..d2961dc8 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -461,7 +461,7 @@ class SearchApiQuery implements SearchApiQueryInterface { return array('#conjunction' => $this->options['conjunction'], $keys); case 'terms': - $ret = explode(' ', $keys); + $ret = preg_split('/\s+/u', $keys); $quoted = FALSE; $str = ''; foreach ($ret as $k => $v) { From c0391605c9ab341bf1aa98a0a1e92816c4bd48e7 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 29 Aug 2013 19:04:06 +0200 Subject: [PATCH 008/278] Issue #1921690 by drunken monkey: Fixed stale Views cache when indexed fields change. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/search_api_views.module | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 41d1154d..d55a7ce7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1921690 by drunken monkey: Fixed stale Views cache when indexed fields + change. - #2077035 by maciej.zgadzaj: Fixed whitespace recognition for search keys. - #2071229 by drunken monkey: Fixed use of core search constant. - #2069023 by drunken monkey: Fixed reaction to disabled modules. diff --git a/contrib/search_api_views/search_api_views.module b/contrib/search_api_views/search_api_views.module index 7a65d730..f559714e 100644 --- a/contrib/search_api_views/search_api_views.module +++ b/contrib/search_api_views/search_api_views.module @@ -21,9 +21,19 @@ function search_api_views_search_api_index_insert(SearchApiIndex $index) { * Implements hook_search_api_index_update(). */ function search_api_views_search_api_index_update(SearchApiIndex $index) { + // Check whether index was disabled. if (!$index->enabled && $index->original->enabled) { _search_api_views_index_unavailable($index); } + + // Check whether the indexed fields changed. + $old_fields = $index->original->options + array('fields' => array()); + $old_fields = $old_fields['fields']; + $new_fields = $index->options + array('fields' => array()); + $new_fields = $new_fields['fields']; + if ($old_fields != $new_fields) { + views_invalidate_cache(); + } } /** From 9ed550e77ef06a53e88bbdc036f2302eddf2fef9 Mon Sep 17 00:00:00 2001 From: "maciej.zgadzaj" Date: Thu, 29 Aug 2013 19:08:29 +0200 Subject: [PATCH 009/278] Issue #1414048 by drunken monkey: Fixed exception in views.inc removes all Search API tables. (Used maciej.zgadzaj as the author since I forgot that in the last-but-one commit.) --- CHANGELOG.txt | 2 ++ contrib/search_api_views/search_api_views.views.inc | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d55a7ce7..6d851e2d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1414048 by drunken monkey: Fixed exception in views.inc removes all Search + API tables. - #1921690 by drunken monkey: Fixed stale Views cache when indexed fields change. - #2077035 by maciej.zgadzaj: Fixed whitespace recognition for search keys. diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 9079b327..1ee99270 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -27,7 +27,13 @@ function search_api_views_views_data() { ); } - $wrapper = $index->entityWrapper(NULL, TRUE); + try { + $wrapper = $index->entityWrapper(NULL, TRUE); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } // Add field handlers and relationships provided by the Entity API. foreach ($wrapper as $key => $property) { From 355d91878628abf5c0496c1b8cc972edafc0d933 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 1 Sep 2013 10:26:00 +0200 Subject: [PATCH 010/278] Adapted CHANGELOG.txt to 1.8 release. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6d851e2d..08feb8b7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,8 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- + +Search API 1.8 (09/01/2013): +---------------------------- - #1414048 by drunken monkey: Fixed exception in views.inc removes all Search API tables. - #1921690 by drunken monkey: Fixed stale Views cache when indexed fields From 4f67dff8f1041aa6a1a965a66ed55b8d224bc011 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 3 Sep 2013 17:25:29 +0200 Subject: [PATCH 011/278] Minor code style fix. --- contrib/search_api_views/includes/handler_filter_language.inc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contrib/search_api_views/includes/handler_filter_language.inc b/contrib/search_api_views/includes/handler_filter_language.inc index f95ddaf9..399f80c1 100644 --- a/contrib/search_api_views/includes/handler_filter_language.inc +++ b/contrib/search_api_views/includes/handler_filter_language.inc @@ -41,6 +41,10 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt */ public function query() { global $language_content; + + if (!is_array($this->value)) { + $this->value = $this->value ? array($this->value) : array(); + } foreach ($this->value as $i => $v) { if ($v == 'current') { $this->value[$i] = $language_content->language; From 44c7c95f838073bdfd7cdac57734cb03ba01fde9 Mon Sep 17 00:00:00 2001 From: leeo Date: Sat, 7 Sep 2013 18:50:06 +0200 Subject: [PATCH 012/278] Issue #2075839 by leeomara, drunken monkey: Added descriptions to field lists for 'Aggregated Fields'. --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 08feb8b7..d70f02c2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2075839 by leeomara, drunken monkey: Added descriptions to field lists for + 'Aggregated Fields'. Search API 1.8 (09/01/2013): ---------------------------- diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 15246863..b8e66c05 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -11,7 +11,11 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { $fields = $this->index->getFields(FALSE); $field_options = array(); foreach ($fields as $name => $field) { - $field_options[$name] = $field['name']; + $field_options[$name] = check_plain($field['name']); + $field_properties[$name] = array( + '#attributes' => array('title' => $name), + '#description' => check_plain($field['description']), + ); } $additional = empty($this->options['fields']) ? array() : $this->options['fields']; @@ -63,14 +67,14 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { foreach (array_keys($types) as $type) { $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type; } - $form['fields'][$name]['fields'] = array( + $form['fields'][$name]['fields'] = array_merge($field_properties, array( '#type' => 'checkboxes', '#title' => t('Contained fields'), '#options' => $field_options, '#default_value' => drupal_map_assoc($field['fields']), '#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')), '#required' => TRUE, - ); + )); $form['fields'][$name]['actions'] = array( '#type' => 'actions', 'remove' => array( From 3c388d1dabac24c8bc489495558e51d87c743407 Mon Sep 17 00:00:00 2001 From: YaronTal Date: Sat, 14 Sep 2013 14:45:31 +0200 Subject: [PATCH 013/278] Issue #2084953 by Yaron Tal: Fixed issue with theme initialization. --- CHANGELOG.txt | 1 + search_api.module | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d70f02c2..3568aaac 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2084953 by Yaron Tal: Fixed issue with theme initialization. - #2075839 by leeomara, drunken monkey: Added descriptions to field lists for 'Aggregated Fields'. diff --git a/search_api.module b/search_api.module index c97ac4d9..fe9b80bc 100644 --- a/search_api.module +++ b/search_api.module @@ -754,7 +754,8 @@ function search_api_system_info_alter(&$info, $file, $type) { $links = array(); foreach ($indexes as $id => $name) { - $links[] = l($name, "admin/config/search/search_api/index/$id"); + $url = url("admin/config/search/search_api/index/$id"); + $links[] = '' . check_plain($name) . ''; } $args = array('!indexes' => implode(', ', $links)); @@ -778,7 +779,8 @@ function search_api_system_info_alter(&$info, $file, $type) { $links = array(); foreach ($servers as $id => $name) { - $links[] = l($name, "admin/config/search/search_api/server/$id"); + $url = url("admin/config/search/search_api/index/$id"); + $links[] = '' . check_plain($name) . ''; } $args = array('!servers' => implode(', ', $links)); From f3e63a6452e054d015ba2b20abc8d80bcbeea503 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 14 Sep 2013 14:49:39 +0200 Subject: [PATCH 014/278] Follow-up to #2084953 by drunken monkey: Fixed path of links to servers in added module dependencies. --- search_api.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_api.module b/search_api.module index fe9b80bc..b92c609a 100644 --- a/search_api.module +++ b/search_api.module @@ -779,7 +779,7 @@ function search_api_system_info_alter(&$info, $file, $type) { $links = array(); foreach ($servers as $id => $name) { - $url = url("admin/config/search/search_api/index/$id"); + $url = url("admin/config/search/search_api/server/$id"); $links[] = '' . check_plain($name) . ''; } From 6382a9cbb91110081670f6acb47951712e68526e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 27 Sep 2013 14:25:52 +0200 Subject: [PATCH 015/278] Issue #2083481 by drunken monkey, nickgs: Added "exclude" option for facets. --- CHANGELOG.txt | 1 + .../plugins/facetapi/adapter.inc | 19 ++++++++++++++- .../plugins/facetapi/query_type_term.inc | 24 +++++++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3568aaac..f1b6d194 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2083481 by drunken monkey, nickgs: Added "exclude" option for facets. - #2084953 by Yaron Tal: Fixed issue with theme initialization. - #2075839 by leeomara, drunken monkey: Added descriptions to field lists for 'Aggregated Fields'. diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index b99f7dcf..23dde9f6 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -196,7 +196,6 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { */ public function settingsForm(&$form, &$form_state) { $facet = $form['#facetapi']['facet']; - $realm = $form['#facetapi']['realm']; $facet_settings = $this->getFacet($facet)->getSettings(); $options = $facet_settings->settings; $search_ids = variable_get('search_api_facets_search_ids', array()); @@ -205,6 +204,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { $form['global']['default_true'] = array( '#type' => 'select', '#title' => t('Display for searches'), + '#prefix' => '
', '#options' => array( TRUE => t('For all except the selected'), FALSE => t('Only for the selected'), @@ -214,6 +214,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { $form['global']['facet_search_ids'] = array( '#type' => 'select', '#title' => t('Search IDs'), + '#suffix' => '
', '#options' => $search_ids, '#size' => min(4, count($search_ids)), '#multiple' => TRUE, @@ -246,9 +247,25 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { '#type' => 'select', '#title' => t('Granularity'), '#description' => t('Determine the maximum drill-down level'), + '#prefix' => '
', + '#suffix' => '
', '#options' => $granularity_options, '#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE, ); } + + // Add an "Exclude" option for terms. + if(!empty($facet['query types']) && in_array('term', $facet['query types'])) { + $form['global']['operator']['#weight'] = -2; + unset($form['global']['operator']['#suffix']); + $form['global']['exclude'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude'), + '#description' => t('Make the search exclude selected facets, instead of restricting it to them.'), + '#suffix' => '', + '#weight' => -1, + '#default_value' => !empty($options['exclude']), + ); + } } } diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 1d0e8ebc..96ad37b6 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -64,19 +64,22 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy * Helper method for setting a facet filter on a query or query filter object. */ protected function addFacetFilter($query_filter, $field, $filter) { + // Test if this filter should be negated. + $settings = $this->adapter->getFacet($this->facet)->getSettings(); + $exclude = !empty($settings->settings['exclude']); // Integer (or other nun-string) filters might mess up some of the following // comparison expressions. $filter = (string) $filter; if ($filter == '!') { - $query_filter->condition($field, NULL); + $query_filter->condition($field, NULL, $exclude ? '<>' : '='); } elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) { $lower = trim(substr($filter, 1, $pos)); $upper = trim(substr($filter, $pos + 4, -1)); if ($lower == '*' && $upper == '*') { - $query_filter->condition($field, NULL, '<>'); + $query_filter->condition($field, NULL, $exclude ? '=' : '<>'); } - else { + elseif (!$exclude) { if ($lower != '*') { // Iff we have a range with two finite boundaries, we set two // conditions (larger than the lower bound and less than the upper @@ -92,9 +95,22 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy $query_filter->condition($field, $upper, '<='); } } + else { + // Same as above, but with inverted logic. + if ($lower != '*') { + if ($upper != '*' && ($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) { + $original_query_filter = $query_filter; + $query_filter = new SearchApiQueryFilter('OR'); + } + $query_filter->condition($field, $lower, '<'); + } + if ($upper != '*') { + $query_filter->condition($field, $upper, '>'); + } + } } else { - $query_filter->condition($field, $filter); + $query_filter->condition($field, $filter, $exclude ? '<>' : '='); } if (isset($original_query_filter)) { $original_query_filter->filter($query_filter); From e13addcc9264b6125bb7e85299b55409012069b6 Mon Sep 17 00:00:00 2001 From: queenvictoria Date: Fri, 27 Sep 2013 14:54:39 +0200 Subject: [PATCH 016/278] Issue #2088905 by queenvictoria, drunken monkey: Fixed handling of Views override_path option. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f1b6d194..ad6f7af4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2088905 by queenvictoria, drunken monkey: Fixed handling of Views + override_path option. - #2083481 by drunken monkey, nickgs: Added "exclude" option for facets. - #2084953 by Yaron Tal: Fixed issue with theme initialization. - #2075839 by leeomara, drunken monkey: Added descriptions to field lists for diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index f695509b..7a5014a3 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -262,6 +262,12 @@ class SearchApiViewsQuery extends views_plugin_query { if (!empty($this->options['search_api_bypass_access'])) { $this->query->setOption('search_api_bypass_access', TRUE); } + + // If the View and the Panel conspire to provide an overridden path then + // pass that through as the base path. + if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) { + $this->query->setOption('search_api_base_path', $this->view->override_path); + } } /** From ea8fd7987bdba83431f53a55f78dcb94b76b4c98 Mon Sep 17 00:00:00 2001 From: "maciej.zgadzaj" Date: Tue, 1 Oct 2013 11:53:11 +0200 Subject: [PATCH 017/278] Issue #2093023 by maciej.zgadzaj: Added Drush commands to enable and disable indexes. --- CHANGELOG.txt | 2 ++ search_api.drush.inc | 85 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ad6f7af4..0acf1a33 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2093023 by maciej.zgadzaj: Added Drush commands to enable and disable + indexes. - #2088905 by queenvictoria, drunken monkey: Fixed handling of Views override_path option. - #2083481 by drunken monkey, nickgs: Added "exclude" option for facets. diff --git a/search_api.drush.inc b/search_api.drush.inc index 8867995d..67e5f855 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -22,6 +22,32 @@ function search_api_drush_command() { 'aliases' => array('sapi-l'), ); + $items['search-api-enable'] = array( + 'description' => 'Enable one or all disabled search_api indexes.', + 'examples' => array( + 'drush searchapi-enable' => dt('Enable all disabled indexes.'), + 'drush sapi-en' => dt('Alias to enable all disabled indexes.'), + 'drush sapi-en 1' => dt('Enable index with the ID !id.', array('!id' => 1)), + ), + 'arguments' => array( + 'index_id' => dt('The numeric ID or machine name of an index to enable.'), + ), + 'aliases' => array('sapi-en'), + ); + + $items['search-api-disable'] = array( + 'description' => 'Disable one or all enabled search_api indexes.', + 'examples' => array( + 'drush searchapi-disable' => dt('Disable all enabled indexes.'), + 'drush sapi-dis' => dt('Alias to disable all enabled indexes.'), + 'drush sapi-dis 1' => dt('Disable index with the ID !id.', array('!id' => 1)), + ), + 'arguments' => array( + 'index_id' => dt('The numeric ID or machine name of an index to disable.'), + ), + 'aliases' => array('sapi-dis'), + ); + $items['search-api-status'] = array( 'description' => 'Show the status of one or all search indexes.', 'examples' => array( @@ -127,6 +153,65 @@ function drush_search_api_list() { drush_print_table($rows); } +/** + * Enable index(es). + * + * @param string|integer $index_id + * The index name or id which should be enabled. + */ +function drush_search_api_enable($index_id = NULL) { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + $indexes = search_api_drush_get_index($index_id); + if (empty($indexes)) { + return; + } + foreach ($indexes as $index) { + if (!$index->enabled) { + drush_log(dt("Enabling index !index and queueing items for indexing.", array('!index' => $index->name)), 'notice'); + if (search_api_index_enable($index->id)) { + drush_log(dt("The index !index was successfully enabled.", array('!index' => $index->name)), 'ok'); + } + else { + drush_log(dt("Error enabling index !index.", array('!index' => $index->name)), 'error'); + } + } + else { + drush_log(dt("The index !index is already enabled.", array('!index' => $index->name)), 'error'); + } + } +} + +/** + * Disable index(es). + * + * @param string|integer $index_id + * The index name or id which should be disabled. + */ +function drush_search_api_disable($index_id = NULL) { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + $indexes = search_api_drush_get_index($index_id); + if (empty($indexes)) { + return; + } + foreach ($indexes as $index) { + if ($index->enabled) { + if (search_api_index_disable($index->id)) { + drush_log(dt("The index !index was successfully disabled.", array('!index' => $index->name)), 'ok'); + } + else { + drush_log(dt("Error disabling index !index.", array('!index' => $index->name)), 'error'); + } + } + else { + drush_log(dt("The index !index is already disabled.", array('!index' => $index->name)), 'error'); + } + } +} + /** * Display index status. */ From b352c3abe5fad45a45a08241ccb842dc2a7a42ab Mon Sep 17 00:00:00 2001 From: andrewbelcher Date: Tue, 1 Oct 2013 17:06:12 +0200 Subject: [PATCH 018/278] Issue #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute callbacks. --- CHANGELOG.txt | 2 + .../includes/handler_argument_date.inc | 99 +++++++++++++++++++ contrib/search_api_views/includes/query.inc | 6 ++ 3 files changed, 107 insertions(+) create mode 100644 contrib/search_api_views/includes/handler_argument_date.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0acf1a33..f0fedf03 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute + callbacks. - #2093023 by maciej.zgadzaj: Added Drush commands to enable and disable indexes. - #2088905 by queenvictoria, drunken monkey: Fixed handling of Views diff --git a/contrib/search_api_views/includes/handler_argument_date.inc b/contrib/search_api_views/includes/handler_argument_date.inc new file mode 100644 index 00000000..4d74b39a --- /dev/null +++ b/contrib/search_api_views/includes/handler_argument_date.inc @@ -0,0 +1,99 @@ +argument. + */ + public function query($group_by = FALSE) { + if (empty($this->value)) { + $this->fillValue(); + } + + if (empty($this->options['not'])) { + $operator = '='; + $conjunction = 'OR'; + } + else { + $operator = '<>'; + $conjunction = 'AND'; + } + + if (!empty($this->argument)) { + $dates = preg_split('/[, ]/', $this->argument); + + if (!empty($dates)) { + $filter = $this->query->createFilter($conjunction); + foreach ($dates as $date) { + $values = explode(';', $date); + + $is_range = (count($values) > 1); + $my_filter = ($is_range ? $this->query->createFilter(empty($this->options['not']) ? 'AND' : 'OR') : $filter); + $range_op = (empty($this->options['not']) ? '>=' : '<'); + $my_filter->condition($this->real_field, $this->getTimestamp($values[0]), $is_range ? $range_op : $operator); + if ($is_range) { + $range_op = (empty($this->options['not']) ? '<=' : '>'); + $my_filter->condition($this->real_field, strtotime('+1 day', $this->getTimestamp($values[1]))-1, $range_op); + $filter->filter($my_filter); + } + } + + $this->query->filter($filter); + } + } + } + + protected function getTimestamp($value) { + if (is_numeric($value)) { + return $value; + } + + $date = new DateTime($value, date_default_timezone_object()); + $date->setTime(0, 0, 0); + return $date->format('U'); + } + + /** + * Get the title this argument will assign the view, given the argument. + */ + public function title() { + if (!empty($this->argument)) { + if (empty($this->value)) { + $this->fillValue(); + } + $terms = array(); + foreach ($this->value as $tid) { + $taxonomy_term = taxonomy_term_load($tid); + if ($taxonomy_term) { + $terms[] = check_plain($taxonomy_term->name); + } + } + + return $terms ? implode(', ', $terms) : check_plain($this->argument); + } + else { + return check_plain($this->argument); + } + } + + /** + * Fill $this->value with data from the argument. + * + * Uses views_break_phrase(), if appropriate. + */ + protected function fillValue() { + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + } + } + +} diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 7a5014a3..e6b41a07 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -291,6 +291,9 @@ class SearchApiViewsQuery extends views_plugin_query { } try { + // Trigger pager pre_execute(). + $this->pager->pre_execute($this->query); + $start = microtime(TRUE); // Execute the search. @@ -310,6 +313,9 @@ class SearchApiViewsQuery extends views_plugin_query { // We shouldn't use $results['performance']['complete'] here, since // extracting the results probably takes considerable time as well. $view->execute_time = microtime(TRUE) - $start; + + // Trigger pager post_execute(). + $this->pager->post_execute($view->results); } catch (Exception $e) { $this->errors[] = $e->getMessage(); From a30e3acd84d5745d4de895d7e9fa7269ea233822 Mon Sep 17 00:00:00 2001 From: thijsvdanker Date: Thu, 10 Oct 2013 13:12:34 +0200 Subject: [PATCH 019/278] Issue #2097559 by thijsvdanker: Fixed the language of created search excerpts. --- CHANGELOG.txt | 1 + includes/processor_highlight.inc | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f0fedf03..5ac26dd6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2097559 by thijsvdanker: Fixed the language of created search excerpts. - #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute callbacks. - #2093023 by maciej.zgadzaj: Added Drush commands to enable and disable diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 979d4267..8ad8d02e 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -164,6 +164,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * contained in them for the given result. */ protected function getFulltextFields(array &$results, $i, $load = TRUE) { + global $language; $data = array(); // Act as if $load is TRUE if we have a loaded item. $load |= !empty($result['entity']); @@ -198,6 +199,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { return $data; } $wrapper = $this->index->entityWrapper($result['entity'], FALSE); + $wrapper->language($language->language); $extracted = search_api_extract_fields($wrapper, $needs_extraction); foreach ($extracted as $field => $info) { From 32fa0f21249bbd155b1fc8490216adbad3162ddc Mon Sep 17 00:00:00 2001 From: aaronbauman Date: Thu, 10 Oct 2013 16:54:40 +0200 Subject: [PATCH 020/278] Issue #2102353 by aaronbauman: Fixed "smaller than" to read "less than". --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/handler_filter.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5ac26dd6..bb22b3e3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2102353 by aaronbauman: Fixed "smaller than" to read "less than". - #2097559 by thijsvdanker: Fixed the language of created search excerpts. - #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute callbacks. diff --git a/contrib/search_api_views/includes/handler_filter.inc b/contrib/search_api_views/includes/handler_filter.inc index 85c66745..2fe5cfc3 100644 --- a/contrib/search_api_views/includes/handler_filter.inc +++ b/contrib/search_api_views/includes/handler_filter.inc @@ -31,8 +31,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter { */ public function operator_options() { return array( - '<' => t('Is smaller than'), - '<=' => t('Is smaller than or equal to'), + '<' => t('Is less than'), + '<=' => t('Is less than or equal to'), '=' => t('Is equal to'), '<>' => t('Is not equal to'), '>=' => t('Is greater than or equal to'), From 9db2baff6a5dea5aeda39b3aa00a8077a4056d21 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 15 Oct 2013 11:56:24 +0200 Subject: [PATCH 021/278] Issue #2111273 by drunken monkey: Fixed Javascript states for exposed filter operator. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter.inc | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bb22b3e3..e985e28b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2111273 by drunken monkey: Fixed Javascript states for exposed filter + operator. - #2102353 by aaronbauman: Fixed "smaller than" to read "less than". - #2097559 by thijsvdanker: Fixed the language of created search excerpts. - #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute diff --git a/contrib/search_api_views/includes/handler_filter.inc b/contrib/search_api_views/includes/handler_filter.inc index 2fe5cfc3..5a81fb1f 100644 --- a/contrib/search_api_views/includes/handler_filter.inc +++ b/contrib/search_api_views/includes/handler_filter.inc @@ -46,8 +46,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter { * Provide a form for setting the filter value. */ public function value_form(&$form, &$form_state) { - while (is_array($this->value)) { - $this->value = $this->value ? array_shift($this->value) : NULL; + while (is_array($this->value) && count($this->value) < 2) { + $this->value = $this->value ? reset($this->value) : NULL; } $form['value'] = array( '#type' => 'textfield', @@ -58,10 +58,19 @@ class SearchApiViewsHandlerFilter extends views_handler_filter { // Hide the value box if the operator is 'empty' or 'not empty'. // Radios share the same selector so we have to add some dummy selector. - $form['value']['#states']['visible'] = array( - ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), - ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), - ); + if (empty($form_state['exposed'])) { + $form['value']['#states']['visible'] = array( + ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), + ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), + ); + } + elseif (!empty($this->options['expose']['use_operator'])) { + $name = $this->options['expose']['operator_id']; + $form['value']['#states']['visible'] = array( + ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'), + ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'), + ); + } } /** From c3c24861660eb68b5e5cbe15da18c968ac9aefe8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 15 Oct 2013 12:18:49 +0200 Subject: [PATCH 022/278] Small documentation comment fix. --- .../search_api_views/search_api_views.views.inc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 1ee99270..89b3c09f 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -145,8 +145,18 @@ function search_api_views_views_data() { } /** - * Helper function that returns an array of handler definitions to add to a - * views field definition. + * Adds handler definitions for a field to a Views data table definition. + * + * Helper method for search_api_views_views_data(). + * + * @param $id + * The internal identifier of the field. + * @param array $field + * Information about the field. + * @param EntityMetadataWrapper $wrapper + * A wrapper providing further metadata about the field. + * @param array $table + * The existing Views data table definition, as a reference. */ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) { $type = $field['type']; From 6824410813e96377147309ae0c37c4cb0a0e165a Mon Sep 17 00:00:00 2001 From: drumm Date: Mon, 21 Oct 2013 11:25:53 +0200 Subject: [PATCH 023/278] Issue #2110315 by drumm, drunken monkey: Added specialized Views filters for users and terms. --- CHANGELOG.txt | 2 + .../includes/handler_filter_entity.inc | 209 +++++++++++++ .../includes/handler_filter_taxonomy_term.inc | 283 ++++++++++++++++++ .../includes/handler_filter_user.inc | 77 +++++ .../search_api_views/search_api_views.info | 3 + .../search_api_views.views.inc | 9 + 6 files changed, 583 insertions(+) create mode 100644 contrib/search_api_views/includes/handler_filter_entity.inc create mode 100644 contrib/search_api_views/includes/handler_filter_taxonomy_term.inc create mode 100644 contrib/search_api_views/includes/handler_filter_user.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e985e28b..1d64b287 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2110315 by drumm, drunken monkey: Added specialized Views filters for users + and terms. - #2111273 by drunken monkey: Fixed Javascript states for exposed filter operator. - #2102353 by aaronbauman: Fixed "smaller than" to read "less than". diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc new file mode 100644 index 00000000..1dbdb29a --- /dev/null +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -0,0 +1,209 @@ + $this->isMultiValued() ? t('Is one of') : t('Is'), + 'all of' => t('Is all of'), + '<>' => $this->isMultiValued() ? t('Is not one of') : t('Is not'), + 'empty' => t('Is empty'), + 'not empty' => t('Is not empty'), + ); + if (!$this->isMultiValued()) { + unset($operators['all of']); + } + return $operators; + } + + /** + * {@inheritdoc} + */ + public function option_definition() { + $options = parent::option_definition(); + + $options['expose']['multiple']['default'] = TRUE; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function value_form(&$form, &$form_state) { + parent::value_form($form, $form_state); + + if (!is_array($this->value)) { + $this->value = $this->value ? array($this->value) : array(); + } + + // Set the correct default value in case the admin-set value is used (and a + // value is present). The value is used if the form is either not exposed, + // or the exposed form wasn't submitted yet (there is + if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) { + $form['value']['#default_value'] = $this->ids_to_strings($this->value); + } + } + + /** + * {@inheritdoc} + */ + public function value_validate($form, &$form_state) { + if (!empty($form['value'])) { + $value = &$form_state['values']['options']['value']; + $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value); + $ids = $this->validate_entity_strings($form['value'], $values); + + if ($ids) { + $value = $ids; + } + } + } + + /** + * {@inheritdoc} + */ + public function accept_exposed_input($input) { + $rc = parent::accept_exposed_input($input); + + if ($rc) { + // If we have previously validated input, override. + if ($this->validated_exposed_input) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + /** + * {@inheritdoc} + */ + public function exposed_validate(&$form, &$form_state) { + if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { + return; + } + + $identifier = $this->options['expose']['identifier']; + $input = $form_state['values'][$identifier]; + + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $this->operator = $this->options['group_info']['group_items'][$input]['operator']; + $input = $this->options['group_info']['group_items'][$input]['value']; + } + + $values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input); + + if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) { + $this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values); + } + else { + $this->validated_exposed_input = FALSE; + } + } + + /** + * Determines whether multiple user names can be entered into this filter. + * + * This is either the case if the form isn't exposed, or if the " Allow + * multiple selections" option is enabled. + * + * @param array $options + * (optional) The options array to use. If not supplied, the options set on + * this filter will be used. + * + * @return bool + * TRUE if multiple values can be entered for this filter, FALSE otherwise. + */ + protected function isMultiValued(array $options = array()) { + $options = $options ? $options : $this->options; + return empty($options['exposed']) || !empty($options['expose']['multiple']); + } + + /** + * {@inheritdoc} + */ + public function admin_summary() { + $value = $this->value; + $this->value = empty($value) ? '' : $this->ids_to_strings($value); + $ret = parent::admin_summary(); + $this->value = $value; + return $ret; + } + + /** + * {@inheritdoc} + */ + public function query() { + if ($this->operator === 'empty') { + $this->query->condition($this->real_field, NULL, '=', $this->options['group']); + } + elseif ($this->operator === 'not empty') { + $this->query->condition($this->real_field, NULL, '<>', $this->options['group']); + } + elseif (is_array($this->value)) { + if (count($this->value) == 1) { + $this->query->condition($this->real_field, reset($this->value), $this->operator, $this->options['group']); + } + else { + $filter = $this->query->createFilter($this->operator === '<>' || $this->operator === 'all of' ? 'AND' : 'OR'); + foreach ($this->value as $value) { + $filter->condition($this->real_field, $value, $this->operator === 'all of' ? '=' : $this->operator); + } + $this->query->filter($filter, $this->options['group']); + } + } + } + +} diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc new file mode 100644 index 00000000..6f224609 --- /dev/null +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -0,0 +1,283 @@ + 'textfield'); + $options['hierarchy'] = array('default' => 0); + $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function extra_options_form(&$form, &$form_state) { + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Selection type'), + '#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')), + '#default_value' => $this->options['type'], + ); + + $form['hierarchy'] = array( + '#type' => 'checkbox', + '#title' => t('Show hierarchy in dropdown'), + '#default_value' => !empty($this->options['hierarchy']), + ); + $form['hierarchy']['#states']['visible'][':input[name="options[type]"]']['value'] = 'select'; + } + + /** + * {@inheritdoc} + */ + public function value_form(&$form, &$form_state) { + parent::value_form($form, $form_state); + + $vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']); + $form['value']['#title'] = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)); + + if ($this->options['type'] == 'textfield') { + $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid; + } + else { + if (!empty($this->options['hierarchy'])) { + $tree = taxonomy_get_tree($vocabulary->vid); + $options = array(); + + if ($tree) { + foreach ($tree as $term) { + $choice = new stdClass(); + $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); + $options[] = $choice; + } + } + } + else { + $options = array(); + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->orderby('tv.weight'); + $query->orderby('tv.name'); + $query->orderby('td.weight'); + $query->orderby('td.name'); + $query->addTag('term_access'); + $query->condition('tv.machine_name', $vocabulary->machine_name); + $result = $query->execute(); + foreach ($result as $term) { + $options[$term->tid] = $term->name; + } + } + + $default_value = (array) $this->value; + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (!empty($this->options['expose']['reduce'])) { + $options = $this->reduce_value_options($options); + + if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { + $default_value = array(); + } + } + + if (empty($this->options['expose']['multiple'])) { + if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { + $default_value = 'All'; + } + elseif (empty($default_value)) { + $keys = array_keys($options); + $default_value = array_shift($keys); + } + // Due to #1464174 there is a chance that array('') was saved in the admin ui. + // Let's choose a safe default value. + elseif ($default_value == array('')) { + $default_value = 'All'; + } + else { + $copy = $default_value; + $default_value = array_shift($copy); + } + } + } + $form['value']['#type'] = 'select'; + $form['value']['#multiple'] = TRUE; + $form['value']['#options'] = $options; + $form['value']['#size'] = min(9, count($options)); + $form['value']['#default_value'] = $default_value; + + if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $default_value; + } + } + } + + /** + * Reduces the available exposed options according to the selection. + */ + protected function reduce_value_options(array $options) { + foreach ($options as $id => $option) { + if (empty($this->options['value'][$id])) { + unset($options[$id]); + } + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function value_validate($form, &$form_state) { + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + return; + } + + parent::value_validate($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // If view is an attachment and is inheriting exposed filters, then assume + // exposed input has already been validated. + if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) { + $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; + } + + // If it's non-required and there's no value don't bother filtering. + if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) { + return FALSE; + } + + return parent::accept_exposed_input($input); + } + + /** + * {@inheritdoc} + */ + public function exposed_validate(&$form, &$form_state) { + if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { + return; + } + + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + $input = $form_state['values'][$this->options['expose']['identifier']]; + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $input = $this->options['group_info']['group_items'][$input]['value']; + } + + if ($input != 'All') { + $this->validated_exposed_input = (array) $input; + } + return; + } + + parent::exposed_validate($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function validate_entity_strings(array &$form, array $values) { + if (empty($values)) { + return array(); + } + + $tids = array(); + $names = array(); + $missing = array(); + foreach ($values as $value) { + $missing[strtolower($value)] = TRUE; + $names[] = $value; + } + + if (!$names) { + return FALSE; + } + + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->condition('td.name', $names); + $query->condition('tv.machine_name', $this->definition['vocabulary']); + $query->addTag('term_access'); + $result = $query->execute(); + foreach ($result as $term) { + unset($missing[strtolower($term->name)]); + $tids[] = $term->tid; + } + + if ($missing) { + if (!empty($this->options['error_message'])) { + form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing))))); + } + else { + // Add a bogus TID which will show an empty result for a positive filter + // and be ignored for an excluding one. + $tids[] = 0; + } + } + + return $tids; + } + + /** + * {@inheritdoc} + */ + public function expose_form(&$form, &$form_state) { + parent::expose_form($form, $form_state); + if ($this->options['type'] != 'select') { + unset($form['expose']['reduce']); + } + $form['error_message'] = array( + '#type' => 'checkbox', + '#title' => t('Display error message'), + '#description' => t('Display an error message if one of the entered terms could not be found.'), + '#default_value' => !empty($this->options['error_message']), + ); + } + + /** + * {@inheritdoc} + */ + protected function ids_to_strings(array $ids) { + return implode(', ', db_select('taxonomy_term_data', 'td') + ->fields('td', array('name')) + ->condition('td.tid', array_filter($ids)) + ->execute() + ->fetchCol()); + } + +} diff --git a/contrib/search_api_views/includes/handler_filter_user.inc b/contrib/search_api_views/includes/handler_filter_user.inc new file mode 100644 index 00000000..a2ef3ea2 --- /dev/null +++ b/contrib/search_api_views/includes/handler_filter_user.inc @@ -0,0 +1,77 @@ +isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete'; + $form['value']['#autocomplete_path'] = $path; + } + + /** + * {@inheritdoc} + */ + protected function ids_to_strings(array $ids) { + $names = array(); + $args[':uids'] = array_filter($ids); + $result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args); + $result = $result->fetchAllKeyed(); + foreach ($ids as $uid) { + if (!$uid) { + $names[] = variable_get('anonymous', t('Anonymous')); + } + elseif (isset($result[$uid])) { + $names[] = $result[$uid]; + } + } + return implode(', ', $names); + } + + /** + * {@inheritdoc} + */ + protected function validate_entity_strings(array &$form, array $values) { + $uids = array(); + $missing = array(); + foreach ($values as $value) { + if (drupal_strtolower($value) === drupal_strtolower(variable_get('anonymous', t('Anonymous')))) { + $uids[] = 0; + } + else { + $missing[strtolower($value)] = $value; + } + } + + if (!$missing) { + return $uids; + } + + $result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing))); + foreach ($result as $account) { + unset($missing[strtolower($account->name)]); + $uids[] = $account->uid; + } + + if ($missing) { + form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing)))); + } + + return $uids; + } + +} diff --git a/contrib/search_api_views/search_api_views.info b/contrib/search_api_views/search_api_views.info index 735ccfa7..edef611c 100644 --- a/contrib/search_api_views/search_api_views.info +++ b/contrib/search_api_views/search_api_views.info @@ -16,10 +16,13 @@ files[] = includes/handler_argument_taxonomy_term.inc files[] = includes/handler_filter.inc files[] = includes/handler_filter_boolean.inc files[] = includes/handler_filter_date.inc +files[] = includes/handler_filter_entity.inc files[] = includes/handler_filter_fulltext.inc files[] = includes/handler_filter_language.inc files[] = includes/handler_filter_options.inc +files[] = includes/handler_filter_taxonomy_term.inc files[] = includes/handler_filter_text.inc +files[] = includes/handler_filter_user.inc files[] = includes/handler_sort.inc files[] = includes/plugin_cache.inc files[] = includes/query.inc diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 89b3c09f..d8787c34 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -191,6 +191,15 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper elseif ($inner_type == 'date') { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate'; } + elseif (isset($field['entity_type']) && $field['entity_type'] === 'user') { + $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterUser'; + } + elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') { + $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm'; + $info = $wrapper->info(); + $field_info = field_info_field($info['name']); + $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary']; + } else { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter'; } From 9a1bda5ff38c9c54decdbeadc5d1566808ca029a Mon Sep 17 00:00:00 2001 From: sergei_brill Date: Tue, 22 Oct 2013 08:19:45 +0200 Subject: [PATCH 024/278] Issue #2102111 by sergei_brill, drunken monkey: Added hook_search_api_views_query_alter(). --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 9 ++++- .../search_api_views/search_api_views.api.php | 34 +++++++++++++++++++ .../search_api_views/search_api_views.info | 1 - 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 contrib/search_api_views/search_api_views.api.php diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1d64b287..559826d7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2102111 by sergei_brill: Added hook_search_api_views_query_alter(). - #2110315 by drumm, drunken monkey: Added specialized Views filters for users and terms. - #2111273 by drunken monkey: Fixed Javascript states for exposed filter diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index e6b41a07..ebb64cef 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -180,7 +180,6 @@ class SearchApiViewsQuery extends views_plugin_query { '#options' => array(), '#default_value' => $this->options['parse_mode'], ); - $modes = array(); foreach ($this->query->parseModes() as $key => $mode) { $form['parse_mode']['#options'][$key] = $mode['name']; if (!empty($mode['description'])) { @@ -270,6 +269,14 @@ class SearchApiViewsQuery extends views_plugin_query { } } + /** + * {@inheritdoc} + */ + public function alter(&$view) { + parent::alter($view); + drupal_alter('search_api_views_query', $view, $this); + } + /** * Executes the query and fills the associated view object with according * values. diff --git a/contrib/search_api_views/search_api_views.api.php b/contrib/search_api_views/search_api_views.api.php new file mode 100644 index 00000000..95a92ca2 --- /dev/null +++ b/contrib/search_api_views/search_api_views.api.php @@ -0,0 +1,34 @@ +name == 'my_view' && is_numeric($view->exposed_raw_input['title']) && $view->exposed_raw_input['title'] > 0) { + // Traverse through the 'where' part of the query. + foreach ($query->where as &$condition_group) { + foreach ($condition_group['conditions'] as &$condition) { + // If this is the part of the query filtering on title, chang the + // condition to filter on node ID. + if (reset($condition) == 'node.title') { + $condition = array('node.nid', $view->exposed_raw_input['title'],'='); + } + } + } + } +} diff --git a/contrib/search_api_views/search_api_views.info b/contrib/search_api_views/search_api_views.info index edef611c..1d39c48a 100644 --- a/contrib/search_api_views/search_api_views.info +++ b/contrib/search_api_views/search_api_views.info @@ -1,4 +1,3 @@ - name = Search views description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments. dependencies[] = search_api From 0e8013547c0f3878768a0bc6ead33d0510a46a68 Mon Sep 17 00:00:00 2001 From: hefox Date: Tue, 22 Oct 2013 08:22:14 +0200 Subject: [PATCH 025/278] Issue #2109537 by hefox, drunken monkey: Added alter hooks for workflow plugin definitions. --- CHANGELOG.txt | 2 ++ search_api.api.php | 31 +++++++++++++++++++++++++++++++ search_api.module | 14 ++++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 559826d7..06e486e8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2109537 by hefox, drunken monkey: Added alter hooks for workflow plugin + definitions. - #2102111 by sergei_brill: Added hook_search_api_views_query_alter(). - #2110315 by drumm, drunken monkey: Added specialized Views filters for users and terms. diff --git a/search_api.api.php b/search_api.api.php index 9c8857e9..d4211262 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -192,6 +192,8 @@ function hook_search_api_data_type_info_alter(array &$infos) { } /** + * Define available data alterations. + * * Registers one or more callbacks that can be called at index time to add * additional data to the indexed items (e.g. comments or attachments to nodes), * alter the data in other forms or remove items from the array. @@ -226,6 +228,21 @@ function hook_search_api_alter_callback_info() { return $callbacks; } +/** + * Alter the available data alterations. + * + * @param array $callbacks + * The callback information to be altered, keyed by callback IDs. + * + * @see hook_search_api_alter_callback_info() + */ +function hook_search_api_alter_callback_info_alter(array &$callbacks) { + if (!empty($callbacks['example_random_alter'])) { + $callbacks['example_random_alter']['name'] = t('Even more random alteration'); + $callbacks['example_random_alter']['class'] = 'ExampleUltraRandomAlter'; + } +} + /** * Registers one or more processors. These are classes implementing the * SearchApiProcessorInterface interface which can be used at index and search @@ -261,6 +278,20 @@ function hook_search_api_processor_info() { return $callbacks; } +/** + * Alter the available processors. + * + * @param array $processors + * The processor information to be altered, keyed by processor IDs. + * + * @see hook_search_api_processor_info() + */ +function hook_search_api_processor_info_alter(array &$processors) { + if (!empty($processors['example_processor'])) { + $processors['example_processor']['weight'] = -20; + } +} + /** * Allows you to log or alter the items that are indexed. * diff --git a/search_api.module b/search_api.module index b92c609a..d71b0eed 100644 --- a/search_api.module +++ b/search_api.module @@ -1696,10 +1696,13 @@ function search_api_get_alter_callbacks() { if (!isset($callbacks)) { $callbacks = module_invoke_all('search_api_alter_callback_info'); - // Initialization of optional entries with default values + // Fill optional settings with default values. foreach ($callbacks as $id => $callback) { - $callbacks[$id] += array('enabled' => TRUE, 'weight' => 0); + $callbacks[$id] += array('weight' => 0); } + + // Invoke alter hook. + drupal_alter('search_api_alter_callback_info', $callbacks); } return $callbacks; @@ -1719,10 +1722,13 @@ function search_api_get_processors() { if (!isset($processors)) { $processors = module_invoke_all('search_api_processor_info'); - // Initialization of optional entries with default values + // Fill optional settings with default values. foreach ($processors as $id => $processor) { - $processors[$id] += array('enabled pre' => TRUE, 'enabled post' => TRUE, 'weight' => 0); + $processors[$id] += array('weight' => 0); } + + // Invoke alter hook. + drupal_alter('search_api_processor_info', $processors); } return $processors; From 559a3c615f58ceccd3b54c9b8f059f1cc287ac1a Mon Sep 17 00:00:00 2001 From: sammys Date: Tue, 22 Oct 2013 17:59:43 +0200 Subject: [PATCH 026/278] Issue #2091499 by sammys, drunken monkey: Added Views contextual filter handler for dates. --- CHANGELOG.txt | 2 + .../includes/handler_argument_date.inc | 148 +++++++++++++----- contrib/search_api_views/includes/query.inc | 8 +- .../search_api_views/search_api_views.info | 1 + .../search_api_views.views.inc | 3 + 5 files changed, 118 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 06e486e8..cd9ee16b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2091499 by sammys, drunken monkey: Added Views contextual filter handler for + dates. - #2109537 by hefox, drunken monkey: Added alter hooks for workflow plugin definitions. - #2102111 by sergei_brill: Added hook_search_api_views_query_alter(). diff --git a/contrib/search_api_views/includes/handler_argument_date.inc b/contrib/search_api_views/includes/handler_argument_date.inc index 4d74b39a..a92c896e 100644 --- a/contrib/search_api_views/includes/handler_argument_date.inc +++ b/contrib/search_api_views/includes/handler_argument_date.inc @@ -1,5 +1,9 @@ argument. + * {@inheritdoc} */ public function query($group_by = FALSE) { if (empty($this->value)) { $this->fillValue(); + if ($this->value === FALSE) { + $this->abort(); + return; + } } + $outer_conjunction = strtoupper($this->operator); + if (empty($this->options['not'])) { $operator = '='; - $conjunction = 'OR'; + $inner_conjunction = 'OR'; } else { $operator = '<>'; - $conjunction = 'AND'; + $inner_conjunction = 'AND'; } - if (!empty($this->argument)) { - $dates = preg_split('/[, ]/', $this->argument); - - if (!empty($dates)) { - $filter = $this->query->createFilter($conjunction); - foreach ($dates as $date) { - $values = explode(';', $date); - + if (!empty($this->value)) { + if (!empty($this->value)) { + $outer_filter = $this->query->createFilter($outer_conjunction); + foreach ($this->value as $value) { + $value_filter = $this->query->createFilter($inner_conjunction); + $values = explode(';', $value); + $values = array_map(array($this, 'getTimestamp'), $values); + if (in_array(FALSE, $values, TRUE)) { + $this->abort(); + return; + } $is_range = (count($values) > 1); - $my_filter = ($is_range ? $this->query->createFilter(empty($this->options['not']) ? 'AND' : 'OR') : $filter); + + $inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter); $range_op = (empty($this->options['not']) ? '>=' : '<'); - $my_filter->condition($this->real_field, $this->getTimestamp($values[0]), $is_range ? $range_op : $operator); + $inner_filter->condition($this->real_field, $values[0], $is_range ? $range_op : $operator); if ($is_range) { $range_op = (empty($this->options['not']) ? '<=' : '>'); - $my_filter->condition($this->real_field, strtotime('+1 day', $this->getTimestamp($values[1]))-1, $range_op); - $filter->filter($my_filter); + $inner_filter->condition($this->real_field, $values[1], $range_op); + $value_filter->filter($inner_filter); } + $outer_filter->filter($value_filter); } - $this->query->filter($filter); + $this->query->filter($outer_filter); } } } - protected function getTimestamp($value) { + /** + * Converts a value to a timestamp, if it isn't one already. + * + * @param string|int $value + * The value to convert. Either a timestamp, or a date/time string as + * recognized by strtotime(). + * + * @return int|false + * The parsed timestamp, or FALSE if an illegal string was passed. + */ + public function getTimestamp($value) { if (is_numeric($value)) { return $value; } - $date = new DateTime($value, date_default_timezone_object()); - $date->setTime(0, 0, 0); - return $date->format('U'); + return strtotime($value); } /** - * Get the title this argument will assign the view, given the argument. + * Fills $this->value with data from the argument. */ - public function title() { - if (!empty($this->argument)) { - if (empty($this->value)) { - $this->fillValue(); + protected function fillValue() { + if (!empty($this->options['break_phrase'])) { + // Set up defaults: + if (!isset($this->value)) { + $this->value = array(); } - $terms = array(); - foreach ($this->value as $tid) { - $taxonomy_term = taxonomy_term_load($tid); - if ($taxonomy_term) { - $terms[] = check_plain($taxonomy_term->name); - } + + if (!isset($this->operator)) { + $this->operator = 'OR'; + } + + if (empty($this->argument)) { + return; + } + + if (preg_match('/^([-\d;:\s]+\+)*[-\d;:\s]+$/', $this->argument)) { + // The '+' character in a query string may be parsed as ' '. + $this->value = explode('+', $this->argument); + } + elseif (preg_match('/^([-\d;:\s]+,)*[-\d;:\s]+$/', $this->argument)) { + $this->operator = 'AND'; + $this->value = explode(',', $this->argument); } - return $terms ? implode(', ', $terms) : check_plain($this->argument); + // Keep an 'error' value if invalid strings were given. + if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) { + $this->value = FALSE; + } } else { - return check_plain($this->argument); + $this->value = array($this->argument); } } /** - * Fill $this->value with data from the argument. + * Aborts the associated query due to an illegal argument. + */ + protected function abort() { + $variables['!field'] = $this->definition['group'] . ': ' . $this->definition['title']; + $this->query->abort(t('Illegal argument passed to !field contextual filter.', $variables)); + } + + /** + * Computes the title this argument will assign the view, given the argument. * - * Uses views_break_phrase(), if appropriate. + * @return string + * A title fitting for the passed argument. */ - protected function fillValue() { - if (!empty($this->options['break_phrase'])) { - views_break_phrase($this->argument, $this); - } - else { - $this->value = array($this->argument); + public function title() { + if (!empty($this->argument)) { + if (empty($this->value)) { + $this->fillValue(); + } + $dates = array(); + foreach ($this->value as $date) { + $date_parts = explode(';', $date); + + $ts = $this->getTimestamp($date_parts[0]); + $datestr = format_date($ts, 'short'); + if (count($date_parts) > 1) { + $ts = $this->getTimestamp($date_parts[1]); + $datestr .= ' - ' . format_date($ts, 'short'); + } + + if ($datestr) { + $dates[] = $datestr; + } + } + + return $dates ? implode(', ', $dates) : check_plain($this->argument); } + + return check_plain($this->argument); } } diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index ebb64cef..85bdb9c0 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -336,8 +336,14 @@ class SearchApiViewsQuery extends views_plugin_query { * * Used by handlers to flag a fatal error which shouldn't be displayed but * still lead to the view returning empty and the search not being executed. + * + * @param string|null $msg + * Optionally, a translated, unescaped error message to display. */ - public function abort() { + public function abort($msg = NULL) { + if ($msg) { + $this->errors[] = $msg; + } $this->abort = TRUE; } diff --git a/contrib/search_api_views/search_api_views.info b/contrib/search_api_views/search_api_views.info index 1d39c48a..d9d7ea5f 100644 --- a/contrib/search_api_views/search_api_views.info +++ b/contrib/search_api_views/search_api_views.info @@ -11,6 +11,7 @@ files[] = includes/handler_argument.inc files[] = includes/handler_argument_fulltext.inc files[] = includes/handler_argument_more_like_this.inc files[] = includes/handler_argument_string.inc +files[] = includes/handler_argument_date.inc files[] = includes/handler_argument_taxonomy_term.inc files[] = includes/handler_filter.inc files[] = includes/handler_filter_boolean.inc diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index d8787c34..2a4e3c90 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -207,6 +207,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper if ($inner_type == 'string' || $inner_type == 'uri') { $table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentString'; } + elseif ($inner_type == 'date') { + $table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentDate'; + } else { $table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument'; } From e981dc1d9eb47bd1b7f750b40bd4198a91a37263 Mon Sep 17 00:00:00 2001 From: mmikitka Date: Tue, 22 Oct 2013 18:01:58 +0200 Subject: [PATCH 027/278] Issue #2109247 by mmikitka, drunken monkey: Exposed the status and module properties to Entity API. --- CHANGELOG.txt | 2 ++ search_api.module | 55 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cd9ee16b..cd91ac61 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2109247 by mmikitka, drunken monkey: Exposed the status and module + properties to Entity API. - #2091499 by sammys, drunken monkey: Added Views contextual filter handler for dates. - #2109537 by hefox, drunken monkey: Added alter hooks for workflow plugin diff --git a/search_api.module b/search_api.module index d71b0eed..3faf2d02 100644 --- a/search_api.module +++ b/search_api.module @@ -383,6 +383,19 @@ function search_api_entity_property_info() { 'description' => t('A flag indicating whether the server is enabled.'), 'schema field' => 'enabled', ), + 'status' => array( + 'label' => t('Status'), + 'type' => 'integer', + 'description' => t('Search API server status property'), + 'schema field' => 'status', + 'options list' => 'search_api_status_options_list', + ), + 'module' => array( + 'label' => t('Module'), + 'type' => 'text', + 'description' => t('The name of the module from which this server originates.'), + 'schema field' => 'module', + ), ); $info['search_api_index']['properties'] = array( 'id' => array( @@ -444,6 +457,19 @@ function search_api_entity_property_info() { 'description' => t('A flag indicating whether the index is read-only.'), 'schema field' => 'read_only', ), + 'status' => array( + 'label' => t('Status'), + 'type' => 'integer', + 'description' => t('Search API index status property'), + 'schema field' => 'status', + 'options list' => 'search_api_status_options_list', + ), + 'module' => array( + 'label' => t('Module'), + 'type' => 'text', + 'description' => t('The name of the module from which this index originates.'), + 'schema field' => 'module', + ), ); return $info; @@ -2297,16 +2323,39 @@ function search_api_index_url(SearchApiIndex $index) { } /** - * Property callback. + * Returns an index's server. + * + * Used as a property getter callback for the index's "server_entity" prioperty + * in search_api_entity_property_info(). + * + * @param SearchApiIndex $index + * The index whose server should be returned. * * @return SearchApiServer - * The server this index currently resides on, or NULL if the index - * is currently unassigned. + * The server this index currently resides on, or NULL if the index is + * currently unassigned. */ function search_api_index_get_server(SearchApiIndex $index) { return $index->server(); } +/** + * Returns an options list for the "status" property. + * + * Used as an options list callback in search_api_entity_property_info(). + * + * @return array + * An array of options, as defined by hook_options_list(). + */ +function search_api_status_options_list() { + return array( + ENTITY_CUSTOM => t('Custom'), + ENTITY_IN_CODE => t('Default'), + ENTITY_OVERRIDDEN => t('Overridden'), + ENTITY_FIXED => t('Fixed'), + ); +} + /** * Inserts a new search index into the database. * From 1e5a75919df012747f16f5e4748e7b0bc428ab0e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 22 Oct 2013 18:03:20 +0200 Subject: [PATCH 028/278] Follow-up to #2111273 by drunken monkey: Fixed Javascript states for exposed options filter operator. --- .../includes/handler_filter_options.inc | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc index 8e1361d1..92545445 100644 --- a/contrib/search_api_views/includes/handler_filter_options.inc +++ b/contrib/search_api_views/includes/handler_filter_options.inc @@ -107,12 +107,22 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { '#size' => min(4, count($this->definition['options'])), '#default_value' => is_array($this->value) ? $this->value : array(), ); - // Hide the value box if operator is 'empty' or 'not empty'. + + // Hide the value box if the operator is 'empty' or 'not empty'. // Radios share the same selector so we have to add some dummy selector. - $form['value']['#states']['visible'] = array( - ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), - ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), - ); + if (empty($form_state['exposed'])) { + $form['value']['#states']['visible'] = array( + ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), + ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), + ); + } + elseif (!empty($this->options['expose']['use_operator'])) { + $name = $this->options['expose']['operator_id']; + $form['value']['#states']['visible'] = array( + ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'), + ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'), + ); + } } /** From c89fd1affbdc22f46722b5396fe680e5b8ac10ae Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 22 Oct 2013 18:05:05 +0200 Subject: [PATCH 029/278] Issue #2114593 by drunken monkey: Added list of floats to test module. --- CHANGELOG.txt | 1 + tests/search_api_test.install | 8 +++++++- tests/search_api_test.module | 23 ++++++++++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cd91ac61..0fbbaa17 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2114593 by drunken monkey: Added list of floats to test module. - #2109247 by mmikitka, drunken monkey: Exposed the status and module properties to Entity API. - #2091499 by sammys, drunken monkey: Added Views contextual filter handler for diff --git a/tests/search_api_test.install b/tests/search_api_test.install index 8dea6ebc..2db73bb2 100644 --- a/tests/search_api_test.install +++ b/tests/search_api_test.install @@ -39,7 +39,13 @@ function search_api_test_schema() { 'description' => 'A comma separated list of keywords.', 'type' => 'varchar', 'length' => 200, - 'not null' => FALSE, + 'not null' => FALSE, + ), + 'prices' => array( + 'description' => 'A comma separated list of prices.', + 'type' => 'varchar', + 'length' => 200, + 'not null' => FALSE, ), ), 'primary key' => array('id'), diff --git a/tests/search_api_test.module b/tests/search_api_test.module index 67e33920..8a4ff828 100644 --- a/tests/search_api_test.module +++ b/tests/search_api_test.module @@ -46,6 +46,9 @@ function search_api_test_insert_item(array $form, array &$form_state) { 'keywords' => array( '#type' => 'textfield', ), + 'prices' => array( + '#type' => 'textfield', + ), 'submit' => array( '#type' => 'submit', '#value' => t('Save'), @@ -74,7 +77,7 @@ function search_api_test_load($id) { * Menu callback for displaying search_api_test entities. */ function search_api_test_view($entity) { - return array('text' => nl2br(check_plain(print_r($entity, TRUE)))); + return nl2br(check_plain(print_r($entity, TRUE))); } /** @@ -169,6 +172,12 @@ function search_api_test_entity_property_info() { 'description' => 'An optional collection of keywords describing the item.', 'getter callback' => 'search_api_test_list_callback', ), + 'prices' => array( + 'label' => 'Prices', + 'type' => 'list', + 'description' => 'An optional list of prices.', + 'getter callback' => 'search_api_test_list_callback', + ), ); return $info; @@ -193,13 +202,17 @@ function search_api_test_parent($entity) { /** * List callback. */ -function search_api_test_list_callback($data) { - //return is_array($entity->keywords) ? $entity->keywords : explode(',', $entity->keywords); +function search_api_test_list_callback($data, array $options, $name) { if (is_array($data)) { - $res = is_array($data['keywords']) ? $data['keywords'] : explode(',', $data['keywords']); + $res = is_array($data[$name]) ? $data[$name] : explode(',', $data[$name]); } else { - $res = is_array($data->keywords) ? $data->keywords : explode(',', $data->keywords); + $res = is_array($data->$name) ? $data->$name : explode(',', $data->$name); + } + if ($name == 'prices') { + foreach ($res as &$x) { + $x = (float) $x; + } } return array_filter($res); } From 602f36e9085314e03aef4f7c3c7983a01ba35a69 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 23 Oct 2013 12:40:42 +0200 Subject: [PATCH 030/278] Issue #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields. --- CHANGELOG.txt | 1 + contrib/search_api_views/search_api_views.views.inc | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0fbbaa17..66095387 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields. - #2114593 by drunken monkey: Added list of floats to test module. - #2109247 by mmikitka, drunken monkey: Exposed the status and module properties to Entity API. diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 2a4e3c90..653b8359 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -28,7 +28,7 @@ function search_api_views_views_data() { } try { - $wrapper = $index->entityWrapper(NULL, TRUE); + $wrapper = $index->entityWrapper(NULL, FALSE); } catch (EntityMetadataWrapperException $e) { watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); @@ -43,6 +43,14 @@ function search_api_views_views_data() { } } + try { + $wrapper = $index->entityWrapper(NULL); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } + // Add handlers for all indexed fields. foreach ($index->getFields() as $key => $field) { $tmp = $wrapper; @@ -69,7 +77,7 @@ function search_api_views_views_data() { if ($group) { // @todo Entity type label instead of $group? $table[$id]['group'] = $group; - $name = t('@field (indexed)', array('@field' => $name)); + $name = t('!field (indexed)', array('!field' => $name)); } $table[$id]['title'] = $name; $table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description']; From 5361183224f5c59a09d03a7c480bad0141e59dac Mon Sep 17 00:00:00 2001 From: moonray Date: Wed, 23 Oct 2013 12:42:46 +0200 Subject: [PATCH 031/278] Issue #2113277 by moonray, drunken monkey: Fixed date facet count for active item. --- CHANGELOG.txt | 1 + .../plugins/facetapi/query_type_date.inc | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 66095387..ca8206bc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2113277 by moonray, drunken monkey: Fixed date facet count for active item. - #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields. - #2114593 by drunken monkey: Added list of floats to test module. - #2109247 by mmikitka, drunken monkey: Exposed the status and module diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index ab3002c1..9a783096 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -121,7 +121,8 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue // Gets active facets, starts building hierarchy. $parent = $gap = NULL; - foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) { + $active_items = $this->adapter->getActiveItems($this->facet); + foreach ($active_items as $value => $item) { // If the item is active, the count is the result set count. $build[$value] = array('#count' => $total); @@ -199,7 +200,9 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue if (!isset($build[$new_value])) { $build[$new_value] = array('#count' => $count); } - else { + // Active items already have their value set because it's the current + // result count. + elseif (!isset($active_items[$new_value])) { $build[$new_value]['#count'] += $count; } From dde60e97d234a84d4504f6e6fa2aa93dadda275b Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 23 Oct 2013 12:44:20 +0200 Subject: [PATCH 032/278] Adapted CHANGELOG.txt to the 1.9 release. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ca8206bc..5d9f3501 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,8 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- + +Search API 1.9 (10/23/2013): +---------------------------- - #2113277 by moonray, drunken monkey: Fixed date facet count for active item. - #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields. - #2114593 by drunken monkey: Added list of floats to test module. From 40f2ca089b1afec3842f70653aa6150d4ceff273 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 24 Oct 2013 16:04:19 +0200 Subject: [PATCH 033/278] Issue #2100191 by drunken monkey, Bojhan: Added an admin description to the Search API landing page. --- CHANGELOG.txt | 2 ++ search_api.module | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d9f3501..d382f21f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2100191 by drunken monkey, Bojhan: Added an admin description to the Search + API landing page. Search API 1.9 (10/23/2013): ---------------------------- diff --git a/search_api.module b/search_api.module index 3faf2d02..395a230b 100644 --- a/search_api.module +++ b/search_api.module @@ -153,6 +153,16 @@ function search_api_menu() { return $items; } +/** + * Implements hook_help(). + */ +function search_api_help($path) { + switch ($path) { + case 'admin/config/search/search_api': + return '

' . t('A search server and search index are used to execute searches. Several indexes can exist per server.
You need at least one server and one index to create searches on your site.') . '

'; + } +} + /** * Implements hook_hook_info(). */ From 5bbf737c3f2d05aee50793fc167ec48e169c885a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 28 Oct 2013 10:27:10 +0100 Subject: [PATCH 034/278] Issue #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for empty $item_ids. --- CHANGELOG.txt | 2 ++ includes/datasource.inc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d382f21f..74905a8f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for + empty $item_ids. - #2100191 by drunken monkey, Bojhan: Added an admin description to the Search API landing page. diff --git a/includes/datasource.inc b/includes/datasource.inc index ba0d2ba8..8646f6a8 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -588,7 +588,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * If any of the indexes doesn't use the same item type as this controller. */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return; } $index_ids = array(); @@ -624,7 +624,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * If any of the indexes doesn't use the same item type as this controller. */ public function trackItemQueued($item_ids, SearchApiIndex $index) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return; } $update = db_update($this->table) From 1bd1469e009ba4cd77d4d2496cc4d3a3053ad865 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 1 Nov 2013 20:23:07 +0100 Subject: [PATCH 035/278] Follow-up to #2110315 by drunken monkey: Fixed Views filter for non-Field API term references. --- .../includes/handler_filter_taxonomy_term.inc | 27 +++++++++++++------ .../search_api_views.views.inc | 14 +++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index 6f224609..7afd30bc 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -16,7 +16,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte * {@inheritdoc} */ public function has_extra_options() { - return TRUE; + return !empty($this->definition['vocabulary']); } /** @@ -25,7 +25,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte public function option_definition() { $options = parent::option_definition(); - $options['type'] = array('default' => 'textfield'); + $options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select'); $options['hierarchy'] = array('default' => 0); $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); @@ -57,14 +57,21 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte public function value_form(&$form, &$form_state) { parent::value_form($form, $form_state); - $vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']); - $form['value']['#title'] = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)); + if (!empty($this->definition['vocabulary'])) { + $vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']); + $title = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)); + } + else { + $vocabulary = FALSE; + $title = t('Select terms'); + } + $form['value']['#title'] = $title; - if ($this->options['type'] == 'textfield') { + if ($vocabulary && $this->options['type'] == 'textfield') { $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid; } else { - if (!empty($this->options['hierarchy'])) { + if ($vocabulary && !empty($this->options['hierarchy'])) { $tree = taxonomy_get_tree($vocabulary->vid); $options = array(); @@ -86,7 +93,9 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $query->orderby('td.weight'); $query->orderby('td.name'); $query->addTag('term_access'); - $query->condition('tv.machine_name', $vocabulary->machine_name); + if ($vocabulary) { + $query->condition('tv.machine_name', $vocabulary->machine_name); + } $result = $query->execute(); foreach ($result as $term) { $options[$term->tid] = $term->name; @@ -231,7 +240,9 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); $query->fields('td'); $query->condition('td.name', $names); - $query->condition('tv.machine_name', $this->definition['vocabulary']); + if (!empty($this->definition['vocabulary'])) { + $query->condition('tv.machine_name', $this->definition['vocabulary']); + } $query->addTag('term_access'); $result = $query->execute(); foreach ($result as $term) { diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 653b8359..e9c9d23f 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -206,7 +206,19 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm'; $info = $wrapper->info(); $field_info = field_info_field($info['name']); - $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary']; + // For the "Parent terms" and "All parent terms" properties, we can + // extrapolate the vocabulary from the parent in the selector. (E.g., + // for "field_tags:parent" we can use the information of "field_tags".) + // Otherwise, we can't include any vocabulary information. + if (!$field_info && ($info['name'] == 'parent' || $info['name'] == 'parents_all')) { + if (!empty($table[$id]['real field'])) { + $parts = explode(':', $table[$id]['real field']); + $field_info = field_info_field($parts[count($parts) - 2]); + } + } + if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) { + $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary']; + } } else { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter'; From c734ecd04b225ccb27a9a435d783fb48ab7b1a6c Mon Sep 17 00:00:00 2001 From: jsacksick Date: Sat, 2 Nov 2013 22:11:39 +0100 Subject: [PATCH 036/278] Issue #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom fulltext field types. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 31 +++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 74905a8f..faf3e8af 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom + fulltext field types. - #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for empty $item_ids. - #2100191 by drunken monkey, Bojhan: Added an admin description to the Search diff --git a/search_api.admin.inc b/search_api.admin.inc index 48e0aeb6..90e387d9 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1480,11 +1480,18 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp // An array of option arrays for types, keyed by nesting level. $types = array(0 => search_api_field_types()); - $fulltext_type = array(0 => 'text'); $entity_types = entity_get_info(); - $default_types = search_api_default_field_types(); $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0')); + $fulltext_types = array(0 => array('text')); + // Add all custom data types with fallback "text" to fulltext types as well. + foreach (search_api_get_data_type_info() as $id => $type) { + if ($type['fallback'] != 'text') { + continue; + } + $fulltext_types[0][] = $id; + } + $form_state['index'] = $index; $form['#theme'] = 'search_api_admin_fields_table'; $form['#tree'] = TRUE; @@ -1518,17 +1525,20 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp '#default_value' => $info['indexed'], ); if (empty($info['entity_type'])) { - // Determine the correct type options (i.e., with the correct nesting level). + // Determine the correct type options (with the correct nesting level). $level = search_api_list_nesting_level($info['type']); if (empty($types[$level])) { $type_prefix = str_repeat('list<', $level); $type_suffix = str_repeat('>', $level); $types[$level] = array(); foreach ($types[0] as $type => $name) { - // We use the singular name for list types, since the user usually doesn't care about the nesting level. + // We use the singular name for list types, since the user usually + // doesn't care about the nesting level. $types[$level][$type_prefix . $type . $type_suffix] = $name; } - $fulltext_type[$level] = $type_prefix . 'text' . $type_suffix; + foreach ($fulltext_types[0] as $type) { + $fulltext_types[$level][] = $type_prefix . $type . $type_suffix; + } } $css_key = '#edit-fields-' . drupal_clean_css_identifier($key); $form['fields'][$key]['type'] = array( @@ -1548,10 +1558,19 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp '#states' => array( 'visible' => array( $css_key . '-indexed' => array('checked' => TRUE), - $css_key . '-type' => array('value' => $fulltext_type[$level]), ), ), ); + // Only add the multiple visible states if the VERSION string is >= 7.14. + // See https://drupal.org/node/1464758. + if (version_compare(VERSION, '7.14', '>=')) { + foreach ($fulltext_types[$level] as $type) { + $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'][] = array('value' => $type); + } + } + else { + $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'] = array('value' => reset($fulltext_types[$level])); + } } else { // This is an entity. From db31e0ca223c04d966363ccfaa279fd3bdc6ac32 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 3 Nov 2013 07:39:03 +0100 Subject: [PATCH 037/278] Issue #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order. --- CHANGELOG.txt | 1 + README.txt | 6 - includes/datasource.inc | 48 ++-- includes/index_entity.inc | 1 - search_api.install | 15 +- search_api.module | 224 ++++++----------- search_api.test | 474 +++++++++++++++++++++++++++++------ tests/search_api_test.module | 93 ++++--- 8 files changed, 537 insertions(+), 325 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index faf3e8af..5d00c434 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order. - #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom fulltext field types. - #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for diff --git a/README.txt b/README.txt index 9ea75ed0..6ea62520 100644 --- a/README.txt +++ b/README.txt @@ -210,12 +210,6 @@ search_api_index_worker_callback_runtime: API will spend indexing (for all indexes combined) in each cron run. The default is 15 seconds. -search_api_batch_per_cron: - By changing this variable, you can define how many batch items are created on - a single cron run. The value is per index, so on a site with 5 indexes with a - cron limit of 100 each, the default value of 10 will load and queue up to 5000 - search items in up to 50 batch items. - Information for developers -------------------------- diff --git a/includes/datasource.inc b/includes/datasource.inc index 8646f6a8..a515d66d 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -160,7 +160,9 @@ interface SearchApiDataSourceControllerInterface { * @param array $indexes * The indexes for which the change should be tracked. * @param $dequeue - * If set to TRUE, also change the status of queued items. + * (deprecated) If set to TRUE, also change the status of queued items. + * The concept of queued items will be removed in the Drupal 8 version of + * this module. * * @throws SearchApiDataSourceException * If any of the indexes doesn't use the same item type as this controller. @@ -180,7 +182,12 @@ interface SearchApiDataSourceControllerInterface { * The index for which the items were queued. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If the index doesn't use the same item type as this controller. + * + * @deprecated + * As of Search API 1.10, the cron queue is not used for indexing anymore, + * therefore this method has become useless. It will be removed in the + * Drupal 8 version of this module. */ public function trackItemQueued($item_ids, SearchApiIndex $index); @@ -189,7 +196,7 @@ interface SearchApiDataSourceControllerInterface { * * @param array $item_ids * The IDs of the indexed items. - * @param SearchApiIndex $indexes + * @param SearchApiIndex $index * The index on which the items were indexed. * * @throws SearchApiDataSourceException @@ -571,24 +578,10 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Set the tracking status of the given items to "changed"/"dirty". - * - * Unless $dequeue is set to TRUE, this operation is ignored for items whose - * status is not "indexed". - * - * @param $item_ids - * Either an array with the IDs of the changed items. Or FALSE to mark all - * items as changed for the given indexes. - * @param array $indexes - * The indexes for which the change should be tracked. - * @param $dequeue - * If set to TRUE, also change the status of queued items. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) { - if (!$this->table || $item_ids === array()) { + if (!$this->table) { return; } $index_ids = array(); @@ -609,22 +602,11 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Set the tracking status of the given items to "queued". - * - * Queued items are not marked as "dirty" even when they are changed, and they - * are not returned by the getChangedItems() method. - * - * @param $item_ids - * Either an array with the IDs of the queued items. Or FALSE to mark all - * items as queued for the given indexes. - * @param SearchApiIndex $index - * The index for which the items were queued. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function trackItemQueued($item_ids, SearchApiIndex $index) { - if (!$this->table || $item_ids === array()) { + $this->checkIndex($index); + if (!$this->table) { return; } $update = db_update($this->table) diff --git a/includes/index_entity.inc b/includes/index_entity.inc index d36b2f8e..dc456666 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -230,7 +230,6 @@ class SearchApiIndex extends Entity { */ public function dequeueItems() { $this->datasource()->stopTracking(array($this)); - _search_api_empty_cron_queue($this); } /** diff --git a/search_api.install b/search_api.install index 8a3366cc..f39608c8 100644 --- a/search_api.install +++ b/search_api.install @@ -330,7 +330,6 @@ function search_api_disable() { // Modules defining entity or item types might have been disabled. Ignore. } } - DrupalQueue::get('search_api_indexing_queue')->deleteQueue(); } /** @@ -813,3 +812,17 @@ function search_api_update_7114() { } } } + +/** + * Switch to indexing without the use of a cron queue. + */ +function search_api_update_7115() { + variable_del('search_api_batch_per_cron'); + DrupalQueue::get('search_api_indexing_queue')->deleteQueue(); + db_update('search_api_item') + ->fields(array( + 'changed' => 1, + )) + ->condition('changed', 0, '<') + ->execute(); +} diff --git a/search_api.module b/search_api.module index 395a230b..2c661980 100644 --- a/search_api.module +++ b/search_api.module @@ -265,50 +265,65 @@ function search_api_permission() { * Will index $options['cron-limit'] items for each enabled index. */ function search_api_cron() { - $queue = DrupalQueue::get('search_api_indexing_queue'); - foreach (search_api_index_load_multiple(FALSE, array('enabled' => TRUE, 'read_only' => 0)) as $index) { - $limit = isset($index->options['cron_limit']) + // Load all enabled, not read-only indexes. + $conditions = array( + 'enabled' => TRUE, + 'read_only' => 0 + ); + $indexes = search_api_index_load_multiple(FALSE, $conditions); + if (!$indexes) { + return; + } + // Remember servers which threw an exception. + $ignored_servers = array(); + // Continue indexing, one batch from each index, until the time is up, but at + // least index one batch per index. + $end = time() + variable_get('search_api_index_worker_callback_runtime', 15); + $first_pass = TRUE; + while (TRUE) { + if (!$indexes) { + break; + } + foreach ($indexes as $id => $index) { + if (!$first_pass && time() >= $end) { + break 2; + } + if (!empty($ignored_servers[$index->server])) { + continue; + } + + $limit = isset($index->options['cron_limit']) ? $index->options['cron_limit'] : SEARCH_API_DEFAULT_CRON_LIMIT; - if ($limit) { - try { - $task = array('index' => $index->machine_name); - // Fetch items to index, do not fetch more than the configured amount - // of batches to be created per cron run to avoid timeouts. - $ids = search_api_get_items_to_index($index, $limit > 0 ? $limit * variable_get('search_api_batch_per_cron', 10) : -1); - if (!$ids) { - continue; + $num = 0; + if ($limit) { + try { + $num = search_api_index_items($index, $limit); + if ($num) { + $variables = array( + '@num' => $num, + '%name' => $index->name + ); + watchdog('search_api', 'Indexed @num items for index %name.', $variables, WATCHDOG_INFO); + } } - $batches = $limit > 0 ? array_chunk($ids, $limit, TRUE) : array($ids); - foreach ($batches as $batch) { - $task['items'] = $batch; - $queue->createItem($task); + catch (SearchApiException $e) { + // Exceptions will probably be caused by the server in most cases. + // Therefore, don't index for any index on this server. + $ignored_servers[$index->server] = TRUE; + watchdog_exception('search_api', $e); } - // Mark items as queued so they won't be inserted into the queue again - // on the next cron run. - search_api_track_item_queued($index, $ids); } - catch (SearchApiException $e) { - watchdog_exception('search_api', $e); + if (!$num) { + // Couldn't index any items => stop indexing for this index in this + // cron run. + unset($indexes[$id]); } } + $first_pass = FALSE; } } -/** - * Implements hook_cron_queue_info(). - * - * Defines a queue for saved searches that should be checked for new items. - */ -function search_api_cron_queue_info() { - return array( - 'search_api_indexing_queue' => array( - 'worker callback' => '_search_api_indexing_queue_process', - 'time' => variable_get('search_api_index_worker_callback_runtime', 15), - ), - ); -} - /** * Implements hook_entity_info(). */ @@ -696,15 +711,6 @@ function search_api_search_api_index_update(SearchApiIndex $index) { $index->queueItems(); } } - - // If the cron batch size changed, empty the cron queue for this index. - $old_cron = $index->original->options + array('cron_limit' => NULL); - $old_cron = $old_cron['cron_limit']; - $new_cron = $index->options + array('cron_limit' => NULL); - $new_cron = $new_cron['cron_limit']; - if ($old_cron !== $new_cron) { - _search_api_empty_cron_queue($index, TRUE); - } } /** @@ -1142,6 +1148,12 @@ function search_api_track_item_change($type, array $item_ids) { * The index on which items were queued. * @param array $item_ids * The ids of the queued items. + * + * @deprecated + * As of Search API 1.10, the cron queue is not used for indexing anymore, + * therefore this function has become useless. It will, along with + * SearchApiDataSourceControllerInterface::trackItemQueued(), be removed in + * the Drupal 8 version of this module. */ function search_api_track_item_queued(SearchApiIndex $index, array $item_ids) { $index->datasource()->trackItemQueued($item_ids, $index); @@ -1278,53 +1290,31 @@ function _search_api_settings_equals($setting1, $setting2) { } /** - * Indexes items for the specified index. Only items marked as changed are - * indexed, in their order of change (if known). + * Indexes items for the specified index. + * + * Only items marked as changed are indexed, in their order of change (if + * known). * * @param SearchApiIndex $index * The index on which items should be indexed. - * @param $limit - * The number of items which should be indexed at most. -1 means no limit. + * @param int $limit + * (optional) The number of items which should be indexed at most. Defaults to + * -1, which means that all changed items should be indexed. + * + * @return int + * Number of successfully indexed items. * * @throws SearchApiException * If any error occurs during indexing. - * - * @return - * Number of successfully indexed items. */ function search_api_index_items(SearchApiIndex $index, $limit = -1) { - // Don't try to index read-only indexes. + // Don't try to index on read-only indexes. if ($index->read_only) { return 0; } - $queue = DrupalQueue::get('search_api_indexing_queue'); - $queue->createQueue(); - $indexed = 0; - $unlimited = $limit < 0; - $release_items = array(); - while (($unlimited || $indexed < $limit) && ($item = $queue->claimItem(30))) { - if ($item->data['index'] === $index->machine_name) { - $indexed += _search_api_indexing_queue_process($item->data); - $queue->deleteItem($item); - } - else { - $release_items[] = $item; - } - } - - foreach ($release_items as $item) { - $queue->releaseItem($item); - } - - if ($unlimited || $indexed < $limit) { - $ids = search_api_get_items_to_index($index, $unlimited ? -1 : $limit - $indexed); - if ($ids) { - $indexed += count(search_api_index_specific_items($index, $ids)); - } - } - - return $indexed; + $ids = search_api_get_items_to_index($index, $limit); + return $ids ? count(search_api_index_specific_items($index, $ids)) : 0; } /** @@ -1337,11 +1327,11 @@ function search_api_index_items(SearchApiIndex $index, $limit = -1) { * @param array $ids * The IDs of the items which should be indexed. * + * @return array + * The IDs of all successfully indexed items. + * * @throws SearchApiException * If any error occurs during indexing. - * - * @return - * The IDs of all successfully indexed items. */ function search_api_index_specific_items(SearchApiIndex $index, array $ids) { $items = $index->loadItems($ids); @@ -2475,42 +2465,6 @@ function search_api_index_reindex($id) { */ function _search_api_index_reindex(SearchApiIndex $index) { $index->datasource()->trackItemChange(FALSE, array($index), TRUE); - _search_api_empty_cron_queue($index); -} - -/** - * Helper method for removing all of an index's jobs from the cron queue. - * - * @param SearchApiIndex $index - * The index whose jobs should be removed. - * @param $mark_changed - * If TRUE, mark all items in the queue as "changed" again. Defaults to FALSE. - */ -function _search_api_empty_cron_queue(SearchApiIndex $index, $mark_changed = FALSE) { - $index_id = $index->machine_name; - $queue = DrupalQueue::get('search_api_indexing_queue'); - $queue->createQueue(); - $ids = array(); - $release_items = array(); - while ($item = $queue->claimItem()) { - if ($item->data['index'] === $index_id) { - $queue->deleteItem($item); - if ($mark_changed) { - $ids = array_merge($ids, $item->data['items']); - } - } - else { - $release_items[] = $item; - } - } - - foreach ($release_items as $item) { - $queue->releaseItem($item); - } - - if ($ids) { - $index->datasource()->trackItemChange($ids, array($index), TRUE); - } } /** @@ -2562,42 +2516,6 @@ function search_api_index_options_list() { return $ret; } -/** - * Cron queue worker callback for indexing some items. - * - * @param array $task - * An associative array containing: - * - index: The ID of the index on which items should be indexed. - * - items: The items that should be indexed. - * - * @return - * The number of successfully indexed items. - */ -function _search_api_indexing_queue_process(array $task) { - $index = search_api_index_load($task['index']); - try { - if ($index && $index->enabled && !$index->read_only && $task['items']) { - $indexed = search_api_index_specific_items($index, $task['items']); - $num = count($indexed); - // If some items couldn't be indexed, mark them as dirty again. - if ($num < count($task['items'])) { - // Believe it or not but this is actually quite faster than the equivalent - // $diff = array_diff($task['items'], $indexed); - $diff = array_keys(array_diff_key(array_flip($task['items']), array_flip($indexed))); - // Mark the items as dirty again. - $index->datasource()->trackItemChange($diff, array($index), TRUE); - } - if ($num) { - watchdog('search_api', t('Indexed @num items for index @name', array('@num' => $num, '@name' => $index->name)), NULL, WATCHDOG_INFO); - } - return $num; - } - } - catch (SearchApiException $e) { - watchdog_exception('search_api', $e); - } -} - /** * Shutdown function which indexes all queued items, if any. */ diff --git a/search_api.test b/search_api.test index 1748627c..f8a506b9 100644 --- a/search_api.test +++ b/search_api.test @@ -1,29 +1,66 @@ assertResponse(200, 'HTTP code 200 returned.'); return $ret; } + /** + * Overrides DrupalWebTestCase::drupalPost(). + * + * Additionally asserts that the HTTP request returned a 200 status code. + */ protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { $ret = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post); $this->assertResponse(200, 'HTTP code 200 returned.'); return $ret; } + /** + * Returns information about this test case. + * + * @return array + * An array with information about this test case. + */ public static function getInfo() { return array( 'name' => 'Test search API framework', @@ -32,24 +69,34 @@ class SearchApiWebTest extends DrupalWebTestCase { ); } + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp('entity', 'search_api', 'search_api_test'); } + /** + * Tests correct admin UI, indexing and search behavior. + * + * We only use a single test method to avoid wasting ressources on setting up + * the test environment multiple times. This will be the only method called + * by the Simpletest framework (since the method name starts with "test"). It + * in turn calls other methdos that set up the environment in a certain way + * and then run tests on it. + */ public function testFramework() { $this->drupalLogin($this->drupalCreateUser(array('administer search_api'))); - // @todo Why is there no default index? - //$this->deleteDefaultIndex(); $this->insertItems(); - $this->checkOverview1(); $this->createIndex(); - $this->insertItems(5); + $this->insertItems(); $this->createServer(); - $this->checkOverview2(); + $this->checkOverview(); $this->enableIndex(); $this->searchNoResults(); $this->indexItems(); $this->searchSuccess(); + $this->checkIndexingOrder(); $this->editServer(); $this->clearIndex(); $this->searchNoResults(); @@ -57,57 +104,44 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->disableModules(); } - protected function deleteDefaultIndex() { - $this->drupalPost('admin/config/search/search_api/index/default_node_index/delete', array(), t('Confirm')); - } - - protected function insertItems($offset = 0) { + /** + * Inserts some test items into the database, via the test module. + * + * @param int $number + * The number of items to insert. + * + * @see insertItem() + */ + protected function insertItems($number = 5) { $count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField(); - $this->insertItem(array( - 'id' => $offset + 1, - 'title' => 'Title 1', - 'body' => 'Body text 1.', - 'type' => 'Item', - )); - $this->insertItem(array( - 'id' => $offset + 2, - 'title' => 'Title 2', - 'body' => 'Body text 2.', - 'type' => 'Item', - )); - $this->insertItem(array( - 'id' => $offset + 3, - 'title' => 'Title 3', - 'body' => 'Body text 3.', - 'type' => 'Item', - )); - $this->insertItem(array( - 'id' => $offset + 4, - 'title' => 'Title 4', - 'body' => 'Body text 4.', - 'type' => 'Page', - )); - $this->insertItem(array( - 'id' => $offset + 5, - 'title' => 'Title 5', - 'body' => 'Body text 5.', - 'type' => 'Page', - )); + for ($i = 1; $i <= $number; ++$i) { + $id = $count + $i; + $this->insertItem(array( + 'id' => $id, + 'title' => "Title $id", + 'body' => "Body text $id.", + 'type' => 'Item', + )); + } $count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() - $count; - $this->assertEqual($count, 5, '5 items successfully inserted.'); + $this->assertEqual($count, $number, "$number items successfully inserted."); } - protected function insertItem($values) { + /** + * Helper function for inserting a single test item. + * + * @param array $values + * The property values of the test item. + * + * @see search_api_test_insert_item() + */ + protected function insertItem(array $values) { $this->drupalPost('search_api_test/insert', $values, t('Save')); } - protected function checkOverview1() { - // This test fails for no apparent reason for drupal.org test bots. - // Commenting them out for now. - //$this->drupalGet('admin/config/search/search_api'); - //$this->assertText(t('There are no search servers or indexes defined yet.'), '"No servers" message is displayed.'); - } - + /** + * Creates a test index via the UI and tests whether this works correctly. + */ protected function createIndex() { $values = array( 'name' => '', @@ -222,6 +256,9 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The index is currently disabled.'), '"Disabled" status displayed.'); } + /** + * Creates a test server via the UI and tests whether this works correctly. + */ protected function createServer() { $values = array( 'name' => '', @@ -264,13 +301,19 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText('search_api_test foo bar', 'Service options displayed.'); } - protected function checkOverview2() { + /** + * Checks whether the server and index are correctly listed in the overview. + */ + protected function checkOverview() { $this->drupalGet('admin/config/search/search_api'); $this->assertText('Search API test server', 'Server displayed.'); $this->assertText('Search API test index', 'Index displayed.'); $this->assertNoText(t('There are no search servers or indexes defined yet.'), '"No servers" message not displayed.'); } + /** + * Moves the index onto the server and enables it. + */ protected function enableIndex() { $values = array( 'server' => $this->server_id, @@ -283,24 +326,61 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The index was successfully enabled.')); } + /** + * Asserts that a search on the index works but yields no results. + * + * This is the case since no items should have been indexed yet. + */ protected function searchNoResults() { - $this->drupalGet('search_api_test/query/' . $this->index_id); - $this->assertText('result count = 0', 'No search results returned without indexing.'); - $this->assertText('results = ()', 'No search results returned without indexing.'); + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 0, 'No search results returned without indexing.'); + $this->assertEqual(array_keys($results['results']), array(), 'No search results returned without indexing.'); + } + + /** + * Executes a search on the test index. + * + * Helper method used for testing search results. + * + * @param int|null $offset + * (optional) The offset for the returned results. + * @param int|null $limit + * (optional) The limit for the returned results. + * + * @return array + * Search results as specified by SearchApiQueryInterface::execute(). + */ + protected function doSearch($offset = NULL, $limit = NULL) { + // Since we change server and index settings via the UI (and, therefore, in + // different page requests), the static cache in this page request + // (executing the tests) will get stale. Therefore, we clear it before + // executing the search. + search_api_index_load($this->index_id, TRUE); + search_api_server_load($this->server_id, TRUE); + + $query = search_api_query($this->index_id); + if ($offset || $limit) { + $query->range($offset, $limit); + } + return $query->execute(); } + /** + * Tests indexing via the UI "Index now" functionality. + * + * Asserts that errors during indexing are handled properly and that the + * status readings work. + */ protected function indexItems() { - $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status"); + $this->checkIndexStatus(); $this->assertText(t('The index is currently enabled.'), '"Enabled" status displayed.'); - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.'); - $this->assertText(t('Index now'), '"Index now" button found.'); - $this->assertText(t('Clear index'), '"Clear index" button found.'); - $this->assertNoText(t('Re-index content'), '"Re-index" button not found.'); // Here we test the indexing + the warning message when some items - // can not be indexed. - // The server refuses (for test purpose) to index items with IDs that are - // multiples of 8 unless the "search_api_test_index_all" variable is set. + // cannot be indexed. + // The server refuses (for test purpose) to index the item that has the same + // ID as the "search_api_test_indexing_break" variable (default: 8). + // Therefore, if we try to index 8 items, only the first seven will be + // successfully indexed and a warning should be displayed. $values = array( 'limit' => 8, ); @@ -308,11 +388,14 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('Successfully indexed @count items.', array('@count' => 7))); $this->assertText(t('1 item could not be indexed. Check the logs for details.'), 'Index errors warning is displayed.'); $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); - $this->assertText(t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => 7, '@total' => 10, '@percentage' => 70)), 'Correct index status displayed.'); - $this->assertText(t('Re-indexing'), '"Re-index" button found.'); + $this->checkIndexStatus(7); // Here we're testing the error message when no item could be indexed. - // The item with ID 8 is still not indexed. + // The item with ID 8 is still not indexed, but it will be the first to be + // indexed now. Therefore, if we try to index a single items, only item 8 + // will be passed to the server, which will reject it and no items will be + // indexed. Since normally this signifies a more serious error than when + // only some items couldn't be indexed, this is handled differently. $values = array( 'limit' => 1, ); @@ -321,8 +404,10 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertNoText(t('1 item could not be indexed. Check the logs for details.'), "Index errors warning isn't displayed."); $this->assertText(t("Couldn't index items. Check the logs for details."), 'Index error is displayed.'); - // Here we test the indexing of all the remaining items. - variable_set('search_api_test_index_all', TRUE); + // No we set the "search_api_test_indexing_break" variable to 0, so all + // items will be indexed. The remaining items (8, 9, 10) should therefore + // be successfully indexed and no warning should show. + variable_set('search_api_test_indexing_break', 0); $values = array( 'limit' => -1, ); @@ -330,20 +415,180 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('Successfully indexed @count items.', array('@count' => 3))); $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed."); $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); - $this->assertText(t('All items have been indexed (@indexed / @total).', array('@indexed' => 10, '@total' => 10)), 'Correct index status displayed.'); - $this->assertNoText(t('Index now'), '"Index now" button no longer displayed.'); + $this->checkIndexStatus(10); + + // Reset the static cache for the server. + search_api_server_load($this->server_id, TRUE); + } + + /** + * Checks whether the index's "Status" tab shows the correct values. + * + * Helper method used by indexItems() and others. + * + * The internal browser will point to the index's "Status" tab after this + * method is called. + * + * @param int $indexed + * (optional) The number of items that should be indexed at the moment. + * Defaults to 0. + * @param int $total + * (optional) The (correct) total number of items. Defaults to 10. + * @param bool $check_buttons + * (optional) Whether to check for the correct presence/absence of buttons. + * Defaults to TRUE. + */ + protected function checkIndexStatus($indexed = 0, $total = 10, $check_buttons = TRUE) { + $url = "admin/config/search/search_api/index/{$this->index_id}/status"; + if (strpos($this->url, $url) === FALSE) { + $this->drupalGet($url); + } + $all = ($indexed == $total); + $correct_status = 'Correct index status displayed.'; + if ($all) { + $this->assertText(t('All items have been indexed (@total / @total).', array('@total' => $total)), $correct_status); + } + elseif (!$indexed) { + $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => $total)), $correct_status); + } + else { + $percentage = (int) (100 * $indexed / $total); + $text = t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', + array( + '@indexed' => $indexed, + '@total' => $total, + '@percentage' => $percentage + )); + $this->assertText($text, $correct_status); + } + + if (!$check_buttons) { + return; + } + + if ($all) { + $this->assertNoText(t('Index now'), '"Index now" form not displayed.'); + } + else { + $this->assertText(t('Index now'), '"Index now" form displayed.'); + } + if ($indexed) { + $this->assertText(t('Re-indexing'), '"Re-indexing" form displayed.'); + } + else { + $this->assertNoText(t('Re-indexing'), '"Re-indexing" form not displayed.'); + } + $this->assertText(t('Clear index'), '"Clear index" form displayed.'); } + /** + * Tests whether searches yield the right results after indexing. + * + * The test server only implements range functionality, no kind of fulltext + * search capabilities, so we can only test for that. + */ protected function searchSuccess() { - $this->drupalGet('search_api_test/query/' . $this->index_id); - $this->assertText('result count = 10', 'Correct search result count returned after indexing.'); - $this->assertText('results = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)', 'Correct search results returned after indexing.'); + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 10, 'Correct search result count returned after indexing.'); + $this->assertEqual(array_keys($results['results']), array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 'Correct search results returned after indexing.'); + + $results = $this->doSearch(2, 4); + $this->assertEqual($results['result count'], 10, 'Correct search result count with ranged query.'); + $this->assertEqual(array_keys($results['results']), array(3, 4, 5, 6), 'Correct search results with ranged query.'); + } - $this->drupalGet('search_api_test/query/' . $this->index_id . '/foo/2/4'); - $this->assertText('result count = 10', 'Correct search result count with ranged query.'); - $this->assertText('results = (3, 4, 5, 6)', 'Correct search results with ranged query.'); + /** + * Tests whether items are indexed in the right order. + * + * The indexing order should always be that new items are indexed before + * changed ones, and only then the changed items in the order of their change. + * + * This method also assures that this behavior is even observed when indexing + * temporarily fails. + * + * @see https://drupal.org/node/2115127 + */ + protected function checkIndexingOrder() { + // Set cron batch size to 1 so not all items will get indexed right away. + // This also ensures that later, when indexing of a single item will be + // rejected by using the "search_api_test_indexing_break" variable, this + // will have the effect of rejecting "all" items of a batch (since that + // batch only consists of a single item). + $values = array( + 'options[cron_limit]' => 1, + ); + $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $values, t('Save settings')); + $this->assertText(t('The search index was successfully edited.')); + + // Manually clear the server's item storage – that way, the items will still + // count as indexed for the Search API, but won't be returned in searches. + // We do this so we have finer-grained control over the order in which items + // are indexed. + search_api_server_load($this->server_id, TRUE)->deleteItems(); + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 0, 'Indexed items were successfully deleted from the server.'); + $this->assertEqual(array_keys($results['results']), array(), 'Indexed items were successfully deleted from the server.'); + + // Now insert some new items, and mark others as changed. Make sure that + // each action has a unique timestamp, so the order will be correct. + $this->drupalGet('search_api_test/touch/8'); + $this->insertItems(1);// item 11 + sleep(1); + $this->drupalGet('search_api_test/touch/2'); + $this->insertItems(1);// item 12 + sleep(1); + $this->drupalGet('search_api_test/touch/5'); + $this->insertItems(1);// item 13 + sleep(1); + $this->drupalGet('search_api_test/touch/8'); + $this->insertItems(1); // item 14 + + // Check whether the status display is right. + $this->checkIndexStatus(7, 14, FALSE); + + // Indexing order should now be: 11, 12, 13, 14, 8, 2, 4. Let's try it out! + // First manually index one item, and see if it's 11. + $values = array( + 'limit' => 1, + ); + $this->drupalPost(NULL, $values, t('Index now')); + $this->assertText(t('Successfully indexed @count item.', array('@count' => 1))); + $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed."); + $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); + $this->checkIndexStatus(8, 14, FALSE); + + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 1, 'Indexing order test 1: correct result count.'); + $this->assertEqual(array_keys($results['results']), array(11), 'Indexing order test 1: correct results.'); + + // Now index with a cron run, but stop at item 8. + variable_set('search_api_test_indexing_break', 8); + $this->cronRun(); + // Now just the four new items should have been indexed. + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 4, 'Indexing order test 2: correct result count.'); + $this->assertEqual(array_keys($results['results']), array(11, 12, 13, 14), 'Indexing order test 2: correct results.'); + + // This time stop at item 5 (should be the last one). + variable_set('search_api_test_indexing_break', 5); + $this->cronRun(); + // Now all new and changed items should have been indexed, except item 5. + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 6, 'Indexing order test 3: correct result count.'); + $this->assertEqual(array_keys($results['results']), array(2, 8, 11, 12, 13, 14), 'Indexing order test 3: correct results.'); + + // Index the remaining item. + variable_set('search_api_test_indexing_break', 0); + $this->cronRun(); + // Now all new and changed items should have been indexed. + $results = $this->doSearch(); + $this->assertEqual($results['result count'], 7, 'Indexing order test 4: correct result count.'); + $this->assertEqual(array_keys($results['results']), array(2, 5, 8, 11, 12, 13, 14), 'Indexing order test 4: correct results.'); } + /** + * Tests whether editing the server works correctly. + */ protected function editServer() { $values = array( 'name' => 'test-name-foo', @@ -357,12 +602,20 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText('test-test-baz', 'Service options changed.'); } + /** + * Tests whether clearing the index works correctly. + */ protected function clearIndex() { $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/status", array(), t('Clear index')); $this->assertText(t('The index was successfully cleared.')); - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.'); + $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 14)), 'Correct index status displayed.'); } + /** + * Tests whether deleting the server works correctly. + * + * The index still lying on the server should be disabled and removed from it. + */ protected function deleteServer() { $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm')); $this->assertNoText('test-name-foo', 'Server no longer listed.'); @@ -370,6 +623,13 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The index is currently disabled.'), 'The index was disabled and removed from the server.'); } + /** + * Tests whether disabling and uninstalling the modules works correctly. + * + * This will disable and uninstall both the test module and the Search API. It + * asserts that this works correctly (since the server has been deleted in + * deleteServer()) and that all associated tables and variables are removed. + */ protected function disableModules() { module_disable(array('search_api_test'), FALSE); $this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.'); @@ -398,8 +658,19 @@ class SearchApiWebTest extends DrupalWebTestCase { */ class SearchApiUnitTest extends DrupalWebTestCase { + /** + * The index used by these tests. + * + * @var SearchApIindex + */ protected $index; + /** + * Overrides DrupalTestCase::assertEqual(). + * + * For arrays, checks whether all array keys are mapped the same in both + * arrays recursively, while ignoring their order. + */ protected function assertEqual($first, $second, $message = '', $group = 'Other') { if (is_array($first) && is_array($second)) { return $this->assertTrue($this->deepEquals($first, $second), $message, $group); @@ -409,6 +680,20 @@ class SearchApiUnitTest extends DrupalWebTestCase { } } + /** + * Tests whether two values are equal. + * + * For arrays, this is done by comparing the key/value pairs recursively + * instead of checking for simple equality. + * + * @param mixed $first + * The first value. + * @param mixed $second + * The second value. + * + * @return bool + * TRUE if the two values are equal, FALSE otherwise. + */ protected function deepEquals($first, $second) { if (!is_array($first) || !is_array($second)) { return $first == $second; @@ -424,6 +709,12 @@ class SearchApiUnitTest extends DrupalWebTestCase { return empty($second); } + /** + * Returns information about this test case. + * + * @return array + * An array with information about this test case. + */ public static function getInfo() { return array( 'name' => 'Test search API components', @@ -432,6 +723,9 @@ class SearchApiUnitTest extends DrupalWebTestCase { ); } + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp('entity', 'search_api'); $this->index = entity_create('search_api_index', array( @@ -455,6 +749,12 @@ class SearchApiUnitTest extends DrupalWebTestCase { )); } + /** + * Tests the functionality of several components of the module. + * + * This is the single test method called by the Simpletest framework. It in + * turn calls other helper methods to test specific functionality. + */ public function testUnits() { $this->checkQueryParseKeys(); $this->checkIgnoreCaseProcessor(); @@ -462,11 +762,13 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->checkHtmlFilter(); } - public function checkQueryParseKeys() { + /** + * Checks whether the keys are parsed correctly by the query class. + */ + protected function checkQueryParseKeys() { $options['parse mode'] = 'direct'; $mode = &$options['parse mode']; $query = new SearchApiQuery($this->index, $options); - $modes = $query->parseModes(); $query->keys('foo'); $this->assertEqual($query->getKeys(), 'foo', '"Direct query" parse mode, test 1.'); @@ -499,8 +801,10 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), '"Multiple terms" parse mode, test 4.'); } - public function checkIgnoreCaseProcessor() { - $types = search_api_field_types(); + /** + * Tests the functionality of the "Ignore case" processor. + */ + protected function checkIgnoreCaseProcessor() { $orig = 'Foo bar BaZ, ÄÖÜÀÁ<>»«.'; $processed = drupal_strtolower($orig); $items = array( @@ -566,7 +870,10 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->assertEqual($query->getFilter()->getFilters(), $filters2, 'Filters were processed correctly.'); } - public function checkTokenizer() { + /** + * Tests the functionality of the "Tokenizer" processor. + */ + protected function checkTokenizer() { $orig = 'Foo bar1 BaZ, La-la-la.'; $processed1 = array( array( @@ -648,7 +955,10 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->assertEqual($query->getKeys(), 'foo"b r b z"foob r1', 'Search keys were processed correctly.'); } - public function checkHtmlFilter() { + /** + * Tests the functionality of the "HTML filter" processor. + */ + protected function checkHtmlFilter() { $orig = <<a test. diff --git a/tests/search_api_test.module b/tests/search_api_test.module index 8a4ff828..0fd527f5 100644 --- a/tests/search_api_test.module +++ b/tests/search_api_test.module @@ -11,15 +11,15 @@ function search_api_test_menu() { 'page arguments' => array('search_api_test_insert_item'), 'access callback' => TRUE, ), - 'search_api_test/%search_api_test' => array( + 'search_api_test/view/%search_api_test' => array( 'title' => 'View item', 'page callback' => 'search_api_test_view', - 'page arguments' => array(1), + 'page arguments' => array(2), 'access callback' => TRUE, ), - 'search_api_test/query/%search_api_index' => array( - 'title' => 'Search query', - 'page callback' => 'search_api_test_query', + 'search_api_test/touch/%search_api_test' => array( + 'title' => 'Mark item as changed', + 'page callback' => 'search_api_test_touch', 'page arguments' => array(2), 'access callback' => TRUE, ), @@ -81,38 +81,10 @@ function search_api_test_view($entity) { } /** - * Menu callback for executing a search. + * Menu callback for marking a "search_api_test" entity as changed. */ -function search_api_test_query(SearchApiIndex $index, $keys = 'foo bar', $offset = 0, $limit = 10, $fields = NULL, $sort = NULL, $filters = NULL) { - $query = $index->query() - ->keys($keys ? $keys : NULL) - ->range($offset, $limit); - if ($fields) { - $query->fields(explode(',', $fields)); - } - if ($sort) { - $sort = explode(',', $sort); - $query->sort($sort[0], $sort[1]); - } - else { - $query->sort('search_api_id', 'ASC'); - } - if ($filters) { - $filters = explode(',', $filters); - foreach ($filters as $filter) { - $filter = explode('=', $filter); - $query->condition($filter[0], $filter[1]); - } - } - $result = $query->execute(); - - $ret = ''; - $ret .= 'result count = ' . (int) $result['result count'] . '
'; - $ret .= 'results = (' . (empty($result['results']) ? '' : implode(', ', array_keys($result['results']))) . ')
'; - $ret .= 'warnings = (' . (empty($result['warnings']) ? '' : '"' . implode('", "', $result['warnings']) . '"') . ')
'; - $ret .= 'ignored = (' . (empty($result['ignored']) ? '' : implode(', ', $result['ignored'])) . ')
'; - $ret .= nl2br(check_plain(print_r($result['performance'], TRUE))); - return $ret; +function search_api_test_touch($entity) { + module_invoke_all('entity_update', $entity, 'search_api_test'); } /** @@ -234,6 +206,11 @@ function search_api_test_search_api_service_info() { */ class SearchApiTestService extends SearchApiAbstractService { + /** + * Overrides SearchApiAbstractService::configurationForm(). + * + * Returns a single text field for testing purposes. + */ public function configurationForm(array $form, array &$form_state) { $form = array( 'test' => array( @@ -249,37 +226,46 @@ class SearchApiTestService extends SearchApiAbstractService { return $form; } + /** + * Implements SearchApiServiceInterface::indexItems(). + * + * Indexes items by storing their IDs in the server's options. + * + * If the "search_api_test_indexing_break" variable is set, the item with + * that ID will not be indexed. + */ public function indexItems(SearchApiIndex $index, array $items) { - // Refuse to index items with IDs that are multiples of 8 unless the - // "search_api_test_index_all" variable is set. - if (variable_get('search_api_test_index_all', FALSE)) { - return $this->index($index, array_keys($items)); - } - $ret = array(); + // Refuse to index the item with the same ID as the + // "search_api_test_indexing_break" variable, if it is set. + $exclude = variable_get('search_api_test_indexing_break', 8); foreach ($items as $id => $item) { - if ($id % 8) { - $ret[] = $id; + if ($id == $exclude) { + unset($items[$id]); } } - return $this->index($index, $ret); - } + $ids = array_keys($items); - protected function index(SearchApiIndex $index, array $ids) { $this->options += array('indexes' => array()); $this->options['indexes'] += array($index->machine_name => array()); $this->options['indexes'][$index->machine_name] += drupal_map_assoc($ids); - sort($this->options['indexes'][$index->machine_name]); + asort($this->options['indexes'][$index->machine_name]); $this->server->save(); + return $ids; } /** - * Override so deleteItems() isn't called which would otherwise lead to the + * Overrides SearchApiAbstractService::preDelete(). + * + * Overridden so deleteItems() isn't called which would otherwise lead to the * server being updated and, eventually, to a notice because there is no * server to be updated anymore. */ public function preDelete() {} + /** + * {@inheritdoc} + */ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) { if ($ids == 'all') { if ($index) { @@ -297,6 +283,12 @@ class SearchApiTestService extends SearchApiAbstractService { $this->server->save(); } + /** + * Implements SearchApiServiceInterface::indexItems(). + * + * Will ignore all query settings except the range, as only the item IDs are + * indexed. + */ public function search(SearchApiQueryInterface $query) { $options = $query->getOptions(); $ret = array(); @@ -328,6 +320,9 @@ class SearchApiTestService extends SearchApiAbstractService { return $ret; } + /** + * {@inheritdoc} + */ public function fieldsUpdated(SearchApiIndex $index) { return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0; } From db1d84f299ef6853256754a4afa109f1345a3104 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 6 Nov 2013 15:27:57 +0100 Subject: [PATCH 038/278] Issue #2100199 by drunken monkey: Merged index tabs for a cleaner look. --- CHANGELOG.txt | 1 + search_api.admin.inc | 324 ++++++++++++++++--------------------------- search_api.module | 45 ++---- search_api.test | 23 +-- 4 files changed, 139 insertions(+), 254 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d00c434..5c279013 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2100199 by drunken monkey: Merged index tabs for a cleaner look. - #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order. - #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom fulltext field types. diff --git a/search_api.admin.inc b/search_api.admin.inc index 90e387d9..0252f877 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -473,10 +473,17 @@ function search_api_admin_server_edit(array $form, array &$form_state, SearchApi $form['options']['#title'] = $class['name']; $form['options']['#description'] = $class['description']; - $form['submit'] = array( + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Save settings'), ); + $form['actions']['delete'] = array( + '#type' => 'submit', + '#value' => t('Delete'), + '#submit' => array('search_api_admin_form_delete_submit'), + '#limit_validation_errors' => array(), + ); return $form; } @@ -506,6 +513,30 @@ function search_api_admin_server_edit_submit(array $form, array &$form_state) { drupal_set_message(t('The search server was successfully edited.')); } +/** + * Form submission handler for search_api_admin_server_edit(). + * + * Handles the 'Delete' button on the server and index edit forms. + * + * @see search_api_admin_server_edit() + * @see search_api_admin_index_edit() + */ +function search_api_admin_form_delete_submit($form, &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + if (isset($form_state['server'])) { + $server = $form_state['server']; + $form_state['redirect'] = array('admin/config/search/search_api/server/' . $server->machine_name . '/delete', array('query' => $destination)); + } + elseif (isset($form_state['index'])) { + $index = $form_state['index']; + $form_state['redirect'] = array('admin/config/search/search_api/index/' . $index->machine_name . '/delete', array('query' => $destination)); + } +} + /** * Form callback showing a form for adding an index. */ @@ -633,16 +664,15 @@ function search_api_admin_add_index_submit(array $form, array &$form_state) { } /** - * Displays an index' details. + * Page callback for displaying an index's status. * * @param SearchApiIndex $index * The index to display. + * @param string|null $action + * (optional) An action to execute for the index. Either "enable" or + * "disable". For "disable", a confirm dialog will be shown. */ -function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NULL) { - if (empty($index)) { - return MENU_NOT_FOUND; - } - +function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { if (!empty($action)) { if ($action == 'enable') { if (isset($_GET['token']) && drupal_valid_token($_GET['token'], $index->machine_name)) { @@ -666,206 +696,100 @@ function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NUL } } - $ret = array( - '#theme' => 'search_api_index', - '#id' => $index->id, - '#name' => $index->name, - '#machine_name' => $index->machine_name, - '#description' => $index->description, - '#item_type' => $index->item_type, - '#enabled' => $index->enabled, - '#server' => $index->server(), - '#options' => $index->options, - '#fields' => $index->getFields(), - '#status' => $index->status, - '#read_only' => $index->read_only, - ); - - return $ret; + return drupal_get_form('search_api_admin_index_status_form', $index); } /** - * Theme function for displaying an index. + * Form function for displaying an index status form. * - * @param array $variables - * An associative array containing: - * - id: The index's id. - * - name: The index' name. - * - machine_name: The index' machine name. - * - description: The index' description. - * - item_type: The type of items stored in this index. - * - enabled: Boolean indicating whether the index is enabled. - * - server: The server this index currently rests on, if any. - * - options: The index' options, like cron limit. - * - fields: All indexed fields of the index. - * - indexed_items: The number of items already indexed in their latest - * version on this index. - * - total_items: The total number of items that have to be indexed for this - * index. - * - status: The entity configuration status (in database, in code, etc.). - * - read_only: Boolean indicating whether this index is read only. + * @param SearchApiIndex $index + * The index whose status should be displayed. */ -function theme_search_api_index(array $variables) { - extract($variables); - - $output = ''; +function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index) { + $enabled = !empty($index->enabled); + $server = $index->server(); - $output .= '

' . check_plain($name) . '

' . "\n"; + $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; + $form_state['index'] = $index; - $output .= '
' . "\n"; + if (!empty($index->description)) { + $form['description']['#markup'] = '

' . nl2br(check_plain($index->description)) . '

'; + } - $output .= '
' . t('Status') . '
' . "\n"; - $output .= '
'; + $form['info']['#prefix'] = '
'; + $form['info']['#suffix'] = '
'; if ($enabled) { - $output .= t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $machine_name . '/disable'))); + $status_message = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $index->machine_name . '/disable'))); } - elseif ($server && $server->enabled) { - $output .= t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/index/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name)))))); + elseif (!empty($server->enabled)) { + $status_message = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/index/' . $index->machine_name . '/enable', array('query' => array('token' => drupal_get_token($index->machine_name)))))); } else { - $output .= t('disabled'); + $status_message = t('disabled'); } - $output .= '
' . "\n"; - - $output .= '
' . t('Machine name') . '
' . "\n"; - $output .= '
' . check_plain($machine_name) . '
' . "\n"; + $form['info']['status']['#markup'] = '
' . t('Status') . '
' . "\n"; + $form['info']['status']['#markup'] .= '
' . $status_message . '
' . "\n"; - $output .= '
' . t('Item type') . '
' . "\n"; - $type = search_api_get_item_type_info($item_type); - $type = $type['name']; - $output .= '
' . check_plain($type) . '
' . "\n"; - - if (!empty($description)) { - $output .= '
' . t('Description') . '
' . "\n"; - $output .= '
' . nl2br(check_plain($description)) . '
' . "\n"; - } + $type = search_api_get_item_type_info($index->item_type); + $form['info']['type']['#markup'] = '
' . t('Item type') . '
' . "\n"; + $form['info']['type']['#markup'] .= '
' . check_plain($type['name']) . '
' . "\n"; if (!empty($server)) { - $output .= '
' . t('Server') . '
' . "\n"; - $output .= '
' . l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); + $form['info']['server']['#markup'] = '
' . t('Server') . '
' . "\n"; + $form['info']['server']['#markup'] .= '
' . l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); if (!empty($server->description)) { - $output .= '

' . nl2br(check_plain($server->description)) . '

'; + $form['info']['server']['#markup'] .= '

' . nl2br(check_plain($server->description)) . '

'; } - $output .= '
' . "\n"; + $form['info']['server']['#markup'] .= '' . "\n"; } - if (!$read_only && !empty($options)) { - $output .= '
' . t('Index options') . '
' . "\n"; - $output .= '
' . "\n"; - $output .= '
' . t('Cron batch size') . '
' . "\n"; - if (empty($options['cron_limit'])) { - $output .= '
' . t("Don't index during cron runs") . '
' . "\n"; - } - elseif ($options['cron_limit'] < 0) { - $output .= '
' . t('Unlimited') . '
' . "\n"; - } - else { - $output .= '
' . format_plural($options['cron_limit'], '1 item per cron batch.', '@count items per cron batch.') . '
' . "\n"; - } - - if (!empty($fields)) { - $fields_list = array(); - foreach ($fields as $name => $field) { - if (search_api_is_text_type($field['type'])) { - $fields_list[] = t('@field (@boost x)', array('@field' => $field['name'], '@boost' => $field['boost'])); - } - else { - $fields_list[] = check_plain($field['name']); - } - } - if ($fields_list) { - $output .= '
' . t('Indexed fields') . '
' . "\n"; - $output .= '
' . implode(', ', $fields_list) . '
' . "\n"; - } - } - - $output .= '
' . "\n"; - } - elseif ($read_only) { - $output .= '
' . t('Read only') . '
' . "\n"; - $output .= '
' . t('This index is read-only.') . '
' . "\n"; - } - - $output .= '
' . t('Configuration status') . '
' . "\n"; - $output .= '
' . "\n"; - $output .= theme('entity_status', array('status' => $status)); - $output .= '
' . "\n"; - - $output .= '
'; - - return $output; -} - -/** - * Form function for displaying an index status form. - * - * @param SearchApiIndex $index - * The index whose status should be displayed. - */ -function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index) { - $enabled = !empty($index->enabled); - $status = search_api_index_status($index); - $server = $index->server(); - - $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; - $form_state['index'] = $index; - - $form['status_message'] = array( - '#type' => 'item', - '#title' => t('Status'), - '#description' => $enabled ? t('The index is currently enabled.') : t('The index is currently disabled.'), - ); - if (!empty($server->enabled)) { - $form['status'] = array( - '#type' => 'submit', - '#value' => $enabled ? t('Disable') : t('Enable'), - ); - } + $form['info']['config_status']['#markup'] = '
' . t('Configuration status') . '
' . "\n"; + $form['info']['config_status']['#markup'] .= '
' . theme('entity_status', array('status' => $index->status)) . '
' . "\n"; if ($index->read_only) { - $form['read_only'] = array( - '#type' => 'item', - '#title' => t('Read only'), - '#description' => t('The index is currently in read-only mode. ' . - 'No new items will be indexed, nor will old ones be deleted.'), - ); + $message = t('The index is currently in read-only mode. No new items will be indexed, nor will old ones be deleted.'); + $form['info']['read_only']['#markup'] = '
' . t('Read only') . '
' . "\n"; + $form['info']['read_only']['#markup'] .= '
' . $message . '
'; return $form; } + $status = search_api_index_status($index); + $form['index'] = array( + '#type' => 'fieldset', + '#title' => t('Indexing status'), + '#description' => t('This index is disabled. No information about the indexing status is available.'), + '#collapsible' => TRUE, + ); if ($enabled) { - $form['progress'] = array( - '#type' => 'item', - '#title' => t('Progress'), - ); $all = ($status['indexed'] == $status['total']); if ($all) { - $form['progress']['#description'] = t('All items have been indexed (@total / @total).', + $form['index']['#description'] = t('All items have been indexed (@total / @total).', array('@total' => $status['total'])); } elseif (!$status['indexed']) { - $form['progress']['#description'] = t('All items still need to be indexed (@total total).', + $form['index']['#description'] = t('All items still need to be indexed (@total total).', array('@total' => $status['total'])); } else { $percentage = (int) (100 * $status['indexed'] / $status['total']); - $form['progress']['#description'] = t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', + $form['index']['#description'] = t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => $status['indexed'], '@total' => $status['total'], '@percentage' => $percentage)); } if (!$all) { - $form['index'] = array( + $form['index']['index'] = array( '#type' => 'fieldset', '#title' => t('Index now'), '#collapsible' => TRUE, ); - $form['index']['settings'] = array( + $form['index']['index']['settings'] = array( '#type' => 'fieldset', '#title' => t('Advanced settings'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); - $form['index']['settings']['limit'] = array( + $form['index']['index']['settings']['limit'] = array( '#type' => 'textfield', '#title' => t('Number of items to index'), '#default_value' => -1, @@ -874,7 +798,7 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea '#description' => t('Number of items to index. Set to -1 for all items.'), ); $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit']; - $form['index']['settings']['batch_size'] = array( + $form['index']['index']['settings']['batch_size'] = array( '#type' => 'textfield', '#title' => t('Number of items per batch run'), '#default_value' => $batch_size, @@ -882,15 +806,15 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea '#attributes' => array('class' => array('search-api-batch-size')), '#description' => t('Number of items per batch run. Set to -1 for all items at once (not recommended). Defaults to the cron batch size of the index.'), ); - $form['index']['button'] = array( + $form['index']['index']['button'] = array( '#type' => 'submit', '#value' => t('Index now'), ); - $form['index']['total'] = array( + $form['index']['index']['total'] = array( '#type' => 'value', '#value' => $status['total'], ); - $form['index']['remaining'] = array( + $form['index']['index']['remaining'] = array( '#type' => 'value', '#value' => $status['total'] - $status['indexed'], ); @@ -898,36 +822,26 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea } if ($server) { - if ($enabled && $status['indexed'] > 0) { - $form['reindex'] = array( - '#type' => 'fieldset', - '#title' => t('Re-indexing'), - '#collapsible' => TRUE, - ); - $form['reindex']['message'] = array( - '#type' => 'item', - '#description' => t('This will add all items to the index again (overwriting the index), but existing items in the index will remain searchable.'), - ); - $form['reindex']['button'] = array( - '#type' => 'submit', - '#value' => t('Re-index content'), - ); - } - - $form['clear'] = array( + $form['index']['reindex'] = array( '#type' => 'fieldset', - '#title' => t('Clear index'), + '#title' => t('Re-indexing'), '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, ); - $form['clear']['message'] = array( + $form['index']['reindex']['message'] = array( '#type' => 'item', - '#description' => t('All items will be deleted from the index and have to be inserted again by normally indexing them. ' . - 'Until all items are re-indexed, searches on this index will return incomplete results.
' . - 'Use with care, in most cases rebuilding the index might be enough.'), + '#description' => t('This will mark all items as "changed" and add them to the index again (overwriting existing data) in subsequent indexing operations.'), ); - $form['clear']['button'] = array( + $form['index']['reindex']['clear'] = array( + '#type' => 'checkbox', + '#title' => t('Also clear data on server'), + '#description' => t('If checked, indexed data on the server will be deleted, too. No results will be returned by searches for this index until items are indexed again.
Use with care, in most cases rebuilding the index might be enough.'), + '#default_value' => FALSE, + ); + $form['index']['reindex']['button'] = array( '#type' => 'submit', - '#value' => t('Clear index'), + '#value' => t('Re-index content'), ); } @@ -965,25 +879,26 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta if (!_search_api_batch_indexing_create($index, $values['batch_size'], $values['limit'], $values['remaining'])) { drupal_set_message(t("Couldn't create a batch, please check the batch size and limit."), 'warning'); } - $redirect = $pre . '/status'; + $redirect = $pre; break; case t('Re-index content'): - if ($index->reindex()) { - drupal_set_message(t('The index was successfully scheduled for re-indexing.')); - } - else { - drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); - } - $redirect = $pre . '/status'; - break; - case t('Clear index'): - if ($index->clear()) { - drupal_set_message(t('The index was successfully cleared.')); + if (empty($values['reindex']['clear'])) { + if ($index->reindex()) { + drupal_set_message(t('The index was successfully scheduled for re-indexing.')); + } + else { + drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); + } } else { - drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); + if ($index->clear()) { + drupal_set_message(t('The index was successfully cleared.')); + } + else { + drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); + } } - $redirect = $pre . '/status'; + $redirect = $pre; break; default: @@ -1070,10 +985,17 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI ), ); - $form['submit'] = array( + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Save settings'), ); + $form['actions']['delete'] = array( + '#type' => 'submit', + '#value' => t('Delete'), + '#submit' => array('search_api_admin_form_delete_submit'), + '#limit_validation_errors' => array(), + ); return $form; } diff --git a/search_api.module b/search_api.module index 2c661980..3cd982b1 100644 --- a/search_api.module +++ b/search_api.module @@ -65,6 +65,7 @@ function search_api_menu() { 'file' => 'search_api.admin.inc', 'weight' => -1, 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, ); $items[$pre . '/server/%search_api_server/delete'] = array( 'title' => 'Delete', @@ -76,7 +77,7 @@ function search_api_menu() { 'access callback' => 'search_api_access_delete_page', 'access arguments' => array(5), 'file' => 'search_api.admin.inc', - 'type' => MENU_LOCAL_TASK, + 'type' => MENU_CALLBACK, ); $items[$pre . '/index/%search_api_index'] = array( 'title' => 'View index', @@ -91,21 +92,11 @@ function search_api_menu() { $items[$pre . '/index/%search_api_index/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => -10, - ); - $items[$pre . '/index/%search_api_index/status'] = array( - 'title' => 'Status', - 'description' => 'Display and work on index status.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('search_api_admin_index_status_form', 5), - 'access arguments' => array('administer search_api'), - 'file' => 'search_api.admin.inc', - 'weight' => -8, - 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + 'weight' => -10, ); $items[$pre . '/index/%search_api_index/edit'] = array( - 'title' => 'Settings', + 'title' => 'Edit', 'description' => 'Edit index settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_api_admin_index_edit', 5), @@ -147,7 +138,7 @@ function search_api_menu() { 'access callback' => 'search_api_access_delete_page', 'access arguments' => array(5), 'file' => 'search_api.admin.inc', - 'type' => MENU_LOCAL_TASK, + 'type' => MENU_CALLBACK, ); return $items; @@ -217,24 +208,6 @@ function search_api_theme() { ), 'file' => 'search_api.admin.inc', ); - $themes['search_api_index'] = array( - 'variables' => array( - 'id' => NULL, - 'name' => '', - 'machine_name' => '', - 'description' => NULL, - 'item_type' => NULL, - 'enabled' => NULL, - 'server' => NULL, - 'options' => array(), - 'fields' => array(), - 'indexed_items' => 0, - 'total_items' => 0, - 'status' => ENTITY_CUSTOM, - 'read_only' => 0, - ), - 'file' => 'search_api.admin.inc', - ); $themes['search_api_admin_item_order'] = array( 'render element' => 'element', 'file' => 'search_api.admin.inc', @@ -2673,13 +2646,11 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, * Batch API finishing callback for the indexing functionality. * * @param boolean $success - * Result of the batch operation. + * Whether the batch finished successfully. * @param array $results - * Results. - * @param array $operations - * Remaining batch operation to process. + * Detailed informations about the result. */ -function _search_api_batch_indexing_finished($success, $results, $operations) { +function _search_api_batch_indexing_finished($success, $results) { // Check if called from drush. if (!empty($results['drush'])) { $drupal_set_message = 'drush_log'; diff --git a/search_api.test b/search_api.test index f8a506b9..a000b1aa 100644 --- a/search_api.test +++ b/search_api.test @@ -250,10 +250,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertTitle('Search API test index | Drupal', 'Correct title when viewing index.'); $this->assertText('An index used for testing.', 'Description displayed.'); $this->assertText('Search API test entity', 'Item type displayed.'); - $this->assertText(format_plural(1, '1 item per cron batch.', '@count items per cron batch.'), 'Cron batch size displayed.'); - - $this->drupalGet("admin/config/search/search_api/index/$id/status"); - $this->assertText(t('The index is currently disabled.'), '"Disabled" status displayed.'); + $this->assertText(t('disabled'), '"Disabled" status displayed.'); } /** @@ -373,7 +370,6 @@ class SearchApiWebTest extends DrupalWebTestCase { */ protected function indexItems() { $this->checkIndexStatus(); - $this->assertText(t('The index is currently enabled.'), '"Enabled" status displayed.'); // Here we test the indexing + the warning message when some items // cannot be indexed. @@ -439,7 +435,7 @@ class SearchApiWebTest extends DrupalWebTestCase { * Defaults to TRUE. */ protected function checkIndexStatus($indexed = 0, $total = 10, $check_buttons = TRUE) { - $url = "admin/config/search/search_api/index/{$this->index_id}/status"; + $url = "admin/config/search/search_api/index/{$this->index_id}"; if (strpos($this->url, $url) === FALSE) { $this->drupalGet($url); } @@ -466,19 +462,13 @@ class SearchApiWebTest extends DrupalWebTestCase { return; } + $this->assertText(t('enabled'), '"Enabled" status displayed.'); if ($all) { $this->assertNoText(t('Index now'), '"Index now" form not displayed.'); } else { $this->assertText(t('Index now'), '"Index now" form displayed.'); } - if ($indexed) { - $this->assertText(t('Re-indexing'), '"Re-indexing" form displayed.'); - } - else { - $this->assertNoText(t('Re-indexing'), '"Re-indexing" form not displayed.'); - } - $this->assertText(t('Clear index'), '"Clear index" form displayed.'); } /** @@ -606,7 +596,7 @@ class SearchApiWebTest extends DrupalWebTestCase { * Tests whether clearing the index works correctly. */ protected function clearIndex() { - $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/status", array(), t('Clear index')); + $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}", array('reindex[clear]' => TRUE), t('Re-index content')); $this->assertText(t('The index was successfully cleared.')); $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 14)), 'Correct index status displayed.'); } @@ -619,8 +609,9 @@ class SearchApiWebTest extends DrupalWebTestCase { protected function deleteServer() { $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm')); $this->assertNoText('test-name-foo', 'Server no longer listed.'); - $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status"); - $this->assertText(t('The index is currently disabled.'), 'The index was disabled and removed from the server.'); + $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}"); + $this->assertNoText(t('Server'), 'The index was removed from the server.'); + $this->assertText(t('disabled'), 'The index was disabled.'); } /** From a7882f5e743335b3cfab57d8ef8663211a11eabb Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 7 Nov 2013 10:52:10 +0100 Subject: [PATCH 039/278] Fixed one comment line length. --- .../includes/handler_filter_taxonomy_term.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index 7afd30bc..02d30686 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -123,8 +123,8 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $keys = array_keys($options); $default_value = array_shift($keys); } - // Due to #1464174 there is a chance that array('') was saved in the admin ui. - // Let's choose a safe default value. + // Due to #1464174 there is a chance that array('') was saved in the + // admin ui. Let's choose a safe default value. elseif ($default_value == array('')) { $default_value = 'All'; } From b7795f2b71fd84c710f535b4ec4770698117baa6 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 7 Nov 2013 15:38:24 +0100 Subject: [PATCH 040/278] Issue #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons. --- CHANGELOG.txt | 1 + search_api.admin.css | 225 ++++++++++++++++++++++++++++++++++++++----- search_api.admin.inc | 85 +++++++++------- search_api.admin.js | 162 +++++++++++++++++++++++++++++-- search_api.module | 60 ++++++++++-- 5 files changed, 457 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5c279013..81bf5ca9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons. - #2100199 by drunken monkey: Merged index tabs for a cleaner look. - #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order. - #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom diff --git a/search_api.admin.css b/search_api.admin.css index 0c49e057..c8baf545 100644 --- a/search_api.admin.css +++ b/search_api.admin.css @@ -1,44 +1,221 @@ - -td.search-api-status { +.search-api-overview td.search-api-status { text-align: center; } -div.search-api-edit-menu { +.search-api-overview td { + vertical-align: top; +} + +.search-api-edit-menu { + position: relative; + vertical-align: top; +} + +.search-api-edit-menu .ctools-dropbutton-processed { position: absolute; - background-color: white; - color: black; - z-index: 999; - border: 1px solid black; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - -khtml-border-radius: 4px; - border-radius: 4px; + top: 0; + right: 0; + left: 0; + border-radius: 11px; + /* We need some background color, or the popup will look weird. + * Since we can't know what theme we are on, we just guess it's "Seven". + */ + background-color: #FFF; } -div.search-api-edit-menu ul { - margin: 0 0.5em; - padding: 0; +.search-api-edit-menu .ctools-dropbutton-processed.open { + left: auto; } -div.search-api-edit-menu ul li { - padding: 0; +tr.even .search-api-edit-menu .ctools-dropbutton-processed { + background-color: #F3F4EE; +} + +/* + * DROPBUTTONS + * + * (Largely copied from D8's dropbutton.css. + */ + +/** + * When a dropbutton has only one option, it is simply a button. + */ +.dropbutton-wrapper, +.dropbutton-wrapper div { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.js .dropbutton-wrapper { + display: block; + min-height: 2em; + position: relative; +} + +.js .dropbutton-wrapper, +.js .dropbutton-widget { + max-width: 100%; +} + +@media screen and (max-width: 600px) { + .js .dropbutton-wrapper { + width: 100%; + } +} + +.js .dropbutton-widget { + position: absolute; +} + +/* UL styles are over-scoped in core, so this selector needs weight parity. */ +.js .dropbutton-widget .dropbutton { + list-style-image: none; list-style-type: none; + margin: 0; + overflow: hidden; + padding: 0; +} + +.js .dropbutton li, +.js .dropbutton a { display: block; } -div.search-api-edit-menu.collapsed { +/** + * The dropbutton styling. + * + * A dropbutton is a widget that displays a list of action links as a button + * with a primary action. Secondary actions are hidden behind a click on a + * twisty arrow. + * + * The arrow is created using border on a zero-width, zero-height span. + * The arrow inherits the link color, but can be overridden with border colors. + */ +.js .dropbutton-multiple .dropbutton-widget { + padding-right: 2em; /* LTR */ +} + +.js[dir="rtl"] .dropbutton-multiple .dropbutton-widget { + padding-left: 2em; + padding-right: 0; +} + +.dropbutton-multiple.open, +.dropbutton-multiple.open .dropbutton-widget { + max-width: none; +} + +.dropbutton-multiple.open { + z-index: 100; +} + +.dropbutton-multiple .dropbutton .secondary-action { display: none; } -.search-api-alter-add-aggregation-fields, -.search-api-checkboxes-list { - max-height: 12em; - overflow: auto; +.dropbutton-multiple.open .dropbutton .secondary-action { + display: block; } -/* Workaround for http://drupal.org/node/1015798 */ -.vertical-tabs fieldset div.fieldset-wrapper fieldset legend { +.dropbutton-toggle { + bottom: 0; display: block; - margin-bottom: 2em; + position: absolute; + right: 0; /* LTR */ + text-indent: 110%; + top: 0; + white-space: nowrap; + width: 2em; +} + +[dir="rtl"] .dropbutton-toggle { + left: 0; + right: auto; +} + +.dropbutton-toggle button { + background: none; + border: 0; + cursor: pointer; + display: block; + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +.dropbutton-arrow { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.3333em 0.3333em 0; + display: block; + height: 0; + line-height: 0; + position: absolute; + right: 40%; /* 0.6667em; */ + /* LTR */ + top: 50%; + margin-top: -0.1666em; + width: 0; + overflow: hidden; +} + +[dir="rtl"] .dropbutton-arrow { + left: 0.6667em; + right: auto; +} + +.dropbutton-multiple.open .dropbutton-arrow { + border-bottom: 0.3333em solid; + border-top-color: transparent; + top: 0.6667em; +} + +.js .dropbutton-widget { + background-color: white; + border: 1px solid #CCC; +} + +.js .dropbutton-widget:hover { + border-color: #B8B8B8; +} + +.dropbutton .dropbutton-action > * { + padding: 0.1em 0.5em; + white-space: nowrap; +} + +.dropbutton .secondary-action { + border-top: 1px solid #E8E8E8; } +.dropbutton-multiple .dropbutton { + border-right: 1px solid #E8E8E8; /* LTR */ +} + +[dir="rtl"] .dropbutton-multiple .dropbutton { + border-left: 1px solid #E8E8E8; + border-right: 0 none; +} + +.dropbutton-multiple .dropbutton .dropbutton-action > * { + margin-right: 0.25em; /* LTR */ +} + +[dir="rtl"] .dropbutton-multiple .dropbutton .dropbutton-action > * { + margin-left: 0.25em; + margin-right: 0; +} + +/* + * MISC + */ + +.search-api-alter-add-aggregation-fields, +.search-api-checkboxes-list { + max-height: 12em; + overflow: auto; +} diff --git a/search_api.admin.inc b/search_api.admin.inc index 0252f877..8f95dee9 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -46,15 +46,10 @@ function search_api_admin_overview() { '#title' => t('disabled'), ); $t_disabled['class'] = array('search-api-status'); - $t_enable = t('enable'); - $t_disable = t('disable'); - $t_edit = t('edit'); + $t_enable = t('Enable'); $pre_server = 'admin/config/search/search_api/server'; $pre_index = 'admin/config/search/search_api/index'; $enable = '/enable'; - $disable = '/disable'; - $edit = '/edit'; - $edit_link_options['attributes']['class'][] = 'search-api-edit-menu-toggle'; foreach ($servers as $server) { $url = $pre_server . '/' . $server->machine_name; $row = array(); @@ -64,10 +59,21 @@ function search_api_admin_overview() { } $row[] = $t_server; $row[] = l($server->name, $url); - $row[] = $server->enabled ? l($t_disable, $url . $disable) : l($t_enable, $url . $enable, array('query' => array('token' => drupal_get_token($server->machine_name)))); - $row[] = l($t_edit, $url . $edit); - $row[] = _search_api_admin_delete_link($server); + $links = array(); + // The "Enable" function has no menu link, since a token is required. We add + // it as the first link, since it will most likely be the most useful link + // for a disabled server. (Same for indexes below.) + if (!$server->enabled) { + $links[] = array( + 'title' => $t_enable, + 'href' => $url . $enable, + 'query' => array('token' => drupal_get_token($server->machine_name)) + ); + } + $links = array_merge($links, menu_contextual_links('search-api-server', $pre_server, array($server->machine_name))); + $row[] = theme('search_api_dropbutton', array('links' => $links)); $rows[] = $row; + if (!empty($indexes[$server->machine_name])) { foreach ($indexes[$server->machine_name] as $index) { $url = $pre_index . '/' . $index->machine_name; @@ -79,14 +85,16 @@ function search_api_admin_overview() { $row[] = ''; $row[] = $t_index; $row[] = l($index->name, $url); - $row[] = $index->enabled - ? l($t_disable, $url . $disable) - : ($server->enabled ? l($t_enable, $url . $enable, array('query' => array('token' => drupal_get_token($index->machine_name)))) : ''); - $row[] = l($t_edit, $url . $edit, $edit_link_options) . - ''; - $row[] = _search_api_admin_delete_link($index); + $links = array(); + if (!$index->enabled && $server->enabled) { + $links[] = array( + 'title' => $t_enable, + 'href' => $url . $enable, + 'query' => array('token' => drupal_get_token($index->machine_name)) + ); + } + $links = array_merge($links, menu_contextual_links('search-api-index', $pre_index, array($index->machine_name))); + $row[] = theme('search_api_dropbutton', array('links' => $links)); $rows[] = $row; } } @@ -101,12 +109,8 @@ function search_api_admin_overview() { } $row[] = array('data' => $t_index, 'colspan' => 2); $row[] = l($index->name, $url); - $row[] = ''; - $row[] = l($t_edit, $url . $edit, $edit_link_options) . - ''; - $row[] = _search_api_admin_delete_link($index); + $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name)); + $row[] = theme('search_api_dropbutton', array('links' => $links)); $rows[] = $row; } } @@ -118,32 +122,41 @@ function search_api_admin_overview() { } $header[] = array('data' => t('Type'), 'colspan' => 2); $header[] = t('Name'); - $header[] = array('data' => t('Operations'), 'colspan' => 3); + $header[] = array('data' => t('Operations')); return array( '#theme' => 'table', '#header' => $header, '#rows' => $rows, + '#attributes' => array('class' => array('search-api-overview')), '#empty' => t('There are no search servers or indexes defined yet.'), ); } /** - * @param Entity $entity - * The index or server for which a link should be generated. + * Returns HTML for a drobutton list of links. + * + * When using this, you have to + * + * @param array $variables + * An associative array containing the following keys: + * - links: An array of links, as expected by theme_links(). * * @return string - * A link to a delete form for the entity, if applicable. + * HTML for the dropbutton link list. */ -function _search_api_admin_delete_link(Entity $entity) { - // Delete link only makes sense if entity is in the database (custom or overridden). - if ($entity->hasStatus(ENTITY_CUSTOM)) { - $type = $entity instanceof SearchApiServer ? 'server' : 'index'; - $url = 'admin/config/search/search_api/' . $type . '/' . $entity->machine_name . '/delete'; - $title = $entity->hasStatus(ENTITY_IN_CODE) ? t('revert') : t('delete'); - return l($title, $url); - } - return ''; +function theme_search_api_dropbutton(array &$variables) { + $base_path = drupal_get_path('module', 'search_api') . '/'; + drupal_add_css($base_path . 'search_api.admin.css'); + drupal_add_js($base_path . 'search_api.admin.js'); + + $variables['attributes']['class'][] = 'dropbutton'; + $list = theme('links', $variables); + return "
+
+ $list +
+
"; } /** diff --git a/search_api.admin.js b/search_api.admin.js index d841ec16..dcb3a77b 100644 --- a/search_api.admin.js +++ b/search_api.admin.js @@ -2,6 +2,9 @@ // Copied from filter.admin.js (function ($) { +/** + * Allows the re-ordering of enabled data alterations and processors. + */ Drupal.behaviors.searchApiStatus = { attach: function (context, settings) { $('.search-api-status-wrapper input.form-checkbox', context).once('search-api-status', function () { @@ -43,19 +46,158 @@ Drupal.behaviors.searchApiStatus = { } }; -Drupal.behaviors.searchApiEditMenu = { +/** + * Processes elements with the .dropbutton class on page load. + */ +Drupal.behaviors.searchApiDropButton = { attach: function (context, settings) { - $('.search-api-edit-menu-toggle', context).click(function (e) { - $menu = $(this).parent().find('.search-api-edit-menu'); - if ($menu.is('.collapsed')) { - $menu.removeClass('collapsed'); + var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton'); + if ($dropbuttons.length) { + //$('.dropbutton-toggle', $dropbuttons).click(dropbuttonClickHandler); + // Initialize all buttons. + for (var i = 0, il = $dropbuttons.length; i < il; i++) { + DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton)); } - else { - $menu.addClass('collapsed'); - } - return false; - }); + // Adds the delegated handler that will toggle dropdowns on click. + $('.dropbutton-toggle', $dropbuttons).click(dropbuttonClickHandler); + } } }; +/** + * Delegated callback for opening and closing dropbutton secondary actions. + */ +function dropbuttonClickHandler(e) { + e.preventDefault(); + $(e.target).closest('.dropbutton-wrapper').toggleClass('open'); +} + +/** + * A DropButton presents an HTML list as a button with a primary action. + * + * All secondary actions beyond the first in the list are presented in a + * dropdown list accessible through a toggle arrow associated with the button. + * + * @param {jQuery} dropbutton + * A jQuery element. + * + * @param {Object} settings + * A list of options including: + * - {String} title: The text inside the toggle link element. This text is + * hidden from visual UAs. + */ +function DropButton(dropbutton, settings) { + // Merge defaults with settings. + var options = $.extend({'title': Drupal.t('List additional actions')}, settings); + var $dropbutton = $(dropbutton); + this.$dropbutton = $dropbutton; + this.$list = $dropbutton.find('.dropbutton'); + // Find actions and mark them. + this.$actions = this.$list.find('li').addClass('dropbutton-action'); + + // Add the special dropdown only if there are hidden actions. + if (this.$actions.length > 1) { + // Identify the first element of the collection. + var $primary = this.$actions.slice(0, 1); + // Identify the secondary actions. + var $secondary = this.$actions.slice(1); + $secondary.addClass('secondary-action'); + // Add toggle link. + $primary.after(Drupal.theme('dropbuttonToggle', options)); + // Bind mouse events. + this.$dropbutton + .addClass('dropbutton-multiple') + /** + * Adds a timeout to close the dropdown on mouseleave. + */ + .bind('mouseleave.dropbutton', $.proxy(this.hoverOut, this)) + /** + * Clears timeout when mouseout of the dropdown. + */ + .bind('mouseenter.dropbutton', $.proxy(this.hoverIn, this)) + /** + * Similar to mouseleave/mouseenter, but for keyboard navigation. + */ + .bind('focusout.dropbutton', $.proxy(this.focusOut, this)) + .bind('focusin.dropbutton', $.proxy(this.focusIn, this)); + } +} + +/** + * Extend the DropButton constructor. + */ +$.extend(DropButton, { + /** + * Store all processed DropButtons. + * + * @type {Array} + */ + dropbuttons: [] +}); + +/** + * Extend the DropButton prototype. + */ +$.extend(DropButton.prototype, { + /** + * Toggle the dropbutton open and closed. + * + * @param {Boolean} show + * (optional) Force the dropbutton to open by passing true or to close by + * passing false. + */ + toggle: function (show) { + var isBool = typeof show === 'boolean'; + show = isBool ? show : !this.$dropbutton.hasClass('open'); + this.$dropbutton.toggleClass('open', show); + }, + + hoverIn: function () { + // Clear any previous timer we were using. + if (this.timerID) { + window.clearTimeout(this.timerID); + } + }, + + hoverOut: function () { + // Wait half a second before closing. + this.timerID = window.setTimeout($.proxy(this, 'close'), 500); + }, + + open: function () { + this.toggle(true); + }, + + close: function () { + this.toggle(false); + }, + + focusOut: function (e) { + this.hoverOut.call(this, e); + }, + + focusIn: function (e) { + this.hoverIn.call(this, e); + } +}); + +$.extend(Drupal.theme, { + /** + * A toggle is an interactive element often bound to a click handler. + * + * @param {Object} options + * - {String} title: (optional) The HTML anchor title attribute and + * text for the inner span element. + * + * @return {String} + * A string representing a DOM fragment. + */ + dropbuttonToggle: function (options) { + return '
  • '; + } +}); + +// Expose constructor in the public space. +Drupal.DropButton = DropButton; + })(jQuery); diff --git a/search_api.module b/search_api.module index 3cd982b1..eed68519 100644 --- a/search_api.module +++ b/search_api.module @@ -53,8 +53,9 @@ function search_api_menu() { ); $items[$pre . '/server/%search_api_server/view'] = array( 'title' => 'View', - 'weight' => -10, 'type' => MENU_DEFAULT_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + 'weight' => -10, ); $items[$pre . '/server/%search_api_server/edit'] = array( 'title' => 'Edit', @@ -67,6 +68,18 @@ function search_api_menu() { 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, ); + $items[$pre . '/server/%search_api_server/disable'] = array( + 'title' => 'Disable', + 'description' => 'Disable index.', + 'page callback' => 'search_api_admin_server_view', + 'page arguments' => array(5, 6), + 'access callback' => 'search_api_access_disable_page', + 'access arguments' => array(5), + 'file' => 'search_api.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 8, + ); $items[$pre . '/server/%search_api_server/delete'] = array( 'title' => 'Delete', 'title callback' => 'search_api_title_delete_page', @@ -77,7 +90,9 @@ function search_api_menu() { 'access callback' => 'search_api_access_delete_page', 'access arguments' => array(5), 'file' => 'search_api.admin.inc', - 'type' => MENU_CALLBACK, + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 10, ); $items[$pre . '/index/%search_api_index'] = array( 'title' => 'View index', @@ -102,9 +117,9 @@ function search_api_menu() { 'page arguments' => array('search_api_admin_index_edit', 5), 'access arguments' => array('administer search_api'), 'file' => 'search_api.admin.inc', - 'weight' => -6, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + 'weight' => -6, ); $items[$pre . '/index/%search_api_index/fields'] = array( 'title' => 'Fields', @@ -113,9 +128,9 @@ function search_api_menu() { 'page arguments' => array('search_api_admin_index_fields', 5), 'access arguments' => array('administer search_api'), 'file' => 'search_api.admin.inc', - 'weight' => -4, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + 'weight' => -4, ); $items[$pre . '/index/%search_api_index/workflow'] = array( 'title' => 'Workflow', @@ -124,9 +139,21 @@ function search_api_menu() { 'page arguments' => array('search_api_admin_index_workflow', 5), 'access arguments' => array('administer search_api'), 'file' => 'search_api.admin.inc', - 'weight' => -2, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + 'weight' => -2, + ); + $items[$pre . '/index/%search_api_index/disable'] = array( + 'title' => 'Disable', + 'description' => 'Disable index.', + 'page callback' => 'search_api_admin_index_view', + 'page arguments' => array(5, 6), + 'access callback' => 'search_api_access_disable_page', + 'access arguments' => array(5), + 'file' => 'search_api.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 8, ); $items[$pre . '/index/%search_api_index/delete'] = array( 'title' => 'Delete', @@ -138,7 +165,9 @@ function search_api_menu() { 'access callback' => 'search_api_access_delete_page', 'access arguments' => array(5), 'file' => 'search_api.admin.inc', - 'type' => MENU_CALLBACK, + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 10, ); return $items; @@ -194,6 +223,12 @@ function search_api_hook_info() { * Implements hook_theme(). */ function search_api_theme() { + $themes['search_api_dropbutton'] = array( + 'variables' => array( + 'links' => array(), + ), + 'file' => 'search_api.admin.inc', + ); $themes['search_api_server'] = array( 'variables' => array( 'id' => NULL, @@ -2134,6 +2169,19 @@ function search_api_title_delete_page(Entity $entity) { return $entity->hasStatus(ENTITY_OVERRIDDEN) ? t('Revert') : t('Delete'); } +/** + * Determines whether the current user can disable a server or index. + * + * @param Entity $entity + * The server or index for which the access to the "disable" page is checked. + * + * @return bool + * TRUE if the "disable" page can be accessed by the user, FALSE otherwise. + */ +function search_api_access_disable_page(Entity $entity) { + return user_access('administer search_api') && !empty($entity->enabled); +} + /** * Access callback for determining if a server's or index' "delete" page should * be accessible. From 725819d618d7f1bb614ca64968c08f60cf52d989 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 7 Nov 2013 16:07:12 +0100 Subject: [PATCH 041/278] Issue #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters". --- CHANGELOG.txt | 1 + README.txt | 6 +++--- includes/callback.inc | 2 +- includes/processor.inc | 2 +- search_api.admin.inc | 8 +++----- search_api.module | 4 ++-- search_api.test | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 81bf5ca9..8f0ca26b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters". - #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons. - #2100199 by drunken monkey: Merged index tabs for a cleaner look. - #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order. diff --git a/README.txt b/README.txt index 6ea62520..2e2f581f 100644 --- a/README.txt +++ b/README.txt @@ -90,7 +90,7 @@ IMPORTANT: Access checks results are displayed – either by only indexing such items, or by filtering appropriately at search time. For search on general site content (item type "Node"), this is already - supported by the Search API. To enable this, go to the index's "Workflow" tab + supported by the Search API. To enable this, go to the index's "Filters" tab and activate the "Node access" data alteration. This will add the necessary field, "Node access information", to the index (which you have to leave as "indexed"). If both this field and "Published" are set to be indexed, access @@ -171,8 +171,8 @@ form at the bottom of the page. For instance, you might want to index the author's username to the indexed data of a node, and you need to add the "Body" entity to the node when you want to index the actual text it contains. -- Index workflow - (Configuration > Search API > [Index name] > Workflow) +- Indexing workflow + (Configuration > Search API > [Index name] > Filters) This page lets you customize how the created index works, and what metadata will be available, by selecting data alterations and processors (see the glossary for diff --git a/includes/callback.inc b/includes/callback.inc index c05260e9..ea161fbd 100644 --- a/includes/callback.inc +++ b/includes/callback.inc @@ -26,7 +26,7 @@ interface SearchApiAlterCallbackInterface { /** * Check whether this data-alter callback is applicable for a certain index. * - * This can be used for hiding the callback on the index's "Workflow" tab. To + * This can be used for hiding the callback on the index's "Filters" tab. To * avoid confusion, you should only use criteria that are immutable, such as * the index's entity type. Also, since this is only used for UI purposes, you * should not completely rely on this to ensure certain index configurations diff --git a/includes/processor.inc b/includes/processor.inc index 1774bf19..1b41f3d8 100644 --- a/includes/processor.inc +++ b/includes/processor.inc @@ -27,7 +27,7 @@ interface SearchApiProcessorInterface { /** * Check whether this processor is applicable for a certain index. * - * This can be used for hiding the processor on the index's "Workflow" tab. To + * This can be used for hiding the processor on the index's "Filters" tab. To * avoid confusion, you should only use criteria that are immutable, such as * the index's item type. Also, since this is only used for UI purposes, you * should not completely rely on this to ensure certain index configurations diff --git a/search_api.admin.inc b/search_api.admin.inc index 8f95dee9..0630818b 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1034,8 +1034,7 @@ function search_api_admin_index_edit_submit(array $form, array &$form_state) { } /** - * Edit an index' workflow (data alter callbacks, pre-/postprocessors, and their - * order). + * Form constructor for editing an index's data alterations and processors. * * @param SearchApiIndex $index * The index to edit. @@ -1378,8 +1377,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) $index->save(); $index->reindex(); - drupal_set_message(t("The search index' workflow was successfully edited. " . - 'All content was scheduled for re-indexing so the new settings can take effect.')); + drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect.")); } else { drupal_set_message(t('No values were changed.')); @@ -1792,7 +1790,7 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields'; } else { - drupal_set_message(t('Please set up the index workflow.')); + drupal_set_message(t('Please set up the indexing workflow.')); $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow'; } return; diff --git a/search_api.module b/search_api.module index eed68519..d5307e44 100644 --- a/search_api.module +++ b/search_api.module @@ -133,8 +133,8 @@ function search_api_menu() { 'weight' => -4, ); $items[$pre . '/index/%search_api_index/workflow'] = array( - 'title' => 'Workflow', - 'description' => 'Edit index workflow.', + 'title' => 'Filters', + 'description' => 'Edit indexing workflow.', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_api_admin_index_workflow', 5), 'access arguments' => array('administer search_api'), diff --git a/search_api.test b/search_api.test index a000b1aa..9e098aab 100644 --- a/search_api.test +++ b/search_api.test @@ -244,7 +244,7 @@ class SearchApiWebTest extends DrupalWebTestCase { 'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:body]' => 1, ); $this->drupalPost(NULL, $values, t('Save configuration')); - $this->assertText(t("The search index' workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.'); + $this->assertText(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.'); $this->drupalGet("admin/config/search/search_api/index/$id"); $this->assertTitle('Search API test index | Drupal', 'Correct title when viewing index.'); From 8723c72d6be368e3e25dd46aa76d26aa638cf6eb Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 8 Nov 2013 08:06:48 +0100 Subject: [PATCH 042/278] Issue #1961120 by drunken monkey: Fixed Views handling of short fulltext keywords. --- CHANGELOG.txt | 1 + .../includes/handler_filter_fulltext.inc | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8f0ca26b..b9f872e8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1961120 by drunken monkey: Fixed Views handling of short fulltext keywords. - #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters". - #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons. - #2100199 by drunken monkey: Merged index tabs for a cleaner look. diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index 952a81c5..db8e0c91 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -33,6 +33,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex $options['operator']['default'] = 'AND'; $options['mode'] = array('default' => 'keys'); + $options['min_length'] = array('default' => ''); $options['fields'] = array('default' => array()); return $options; @@ -75,6 +76,55 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex if (isset($form['expose'])) { $form['expose']['#weight'] = -5; } + + $form['min_length'] = array( + '#title' => t('Minimum keyword length'), + '#description' => t('Minimum length of each word in the search keys. Leave empty to allow all words.'), + '#type' => 'textfield', + '#element_validate' => array('element_validate_integer_positive'), + '#default_value' => $this->options['min_length'], + ); + } + + /** + * {@inheritdoc} + */ + public function exposed_validate(&$form, &$form_state) { + // Only validate exposed input. + if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { + return; + } + + // We only need to validate if there is a minimum word length set. + if ($this->options['min_length'] < 2) { + return; + } + + $identifier = $this->options['expose']['identifier']; + $input = &$form_state['values'][$identifier]; + + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $this->operator = $this->options['group_info']['group_items'][$input]['operator']; + $input = &$this->options['group_info']['group_items'][$input]['value']; + } + + // If there is no input, we're fine. + if (!trim($input)) { + return; + } + + $words = preg_split('/\s+/', $input); + foreach ($words as $i => $word) { + if (drupal_strlen($word) < $this->options['min_length']) { + unset($words[$i]); + } + } + if (!$words) { + $vars['@count'] = $this->options['min_length']; + $msg = t('You must include at least one positive keyword with @count characters or more.', $vars); + form_error($form[$identifier], $msg); + } + $input = implode(' ', $words); } /** From 2a4b08df918f24e5d8b2182347548b7794e86c25 Mon Sep 17 00:00:00 2001 From: mxr576 Date: Fri, 8 Nov 2013 08:09:26 +0100 Subject: [PATCH 043/278] Issue #2118589 by mxr576, drunken monkey: Added node access for comment indexes. --- CHANGELOG.txt | 1 + includes/callback_comment_access.inc | 46 ++++++++++++ includes/callback_node_access.inc | 55 ++++++--------- search_api.info | 1 + search_api.module | 101 ++++++++++++++++++--------- 5 files changed, 137 insertions(+), 67 deletions(-) create mode 100644 includes/callback_comment_access.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b9f872e8..0281dacc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2118589 by mxr576, drunken monkey: Added node access for comment indexes. - #1961120 by drunken monkey: Fixed Views handling of short fulltext keywords. - #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters". - #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons. diff --git a/includes/callback_comment_access.inc b/includes/callback_comment_access.inc new file mode 100644 index 00000000..e6273530 --- /dev/null +++ b/includes/callback_comment_access.inc @@ -0,0 +1,46 @@ +getEntityType() === 'comment'; + } + + /** + * Overrides SearchApiAlterNodeAccess::getNode(). + * + * Returns the comment's node, instead of the item (i.e., the comment) itself. + */ + protected function getNode($item) { + return node_load($item->nid); + } + + /** + * Overrides SearchApiAlterNodeAccess::configurationFormSubmit(). + * + * Doesn't index the comment's "Author". + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + $old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_comment_access']['status']); + $new_status = !empty($form_state['values']['callbacks']['search_api_alter_comment_access']['status']); + + if (!$old_status && $new_status) { + $form_state['index']->options['fields']['status']['type'] = 'boolean'; + } + + return parent::configurationFormSubmit($form, $values, $form_state); + } + +} diff --git a/includes/callback_node_access.inc b/includes/callback_node_access.inc index 5acc76c1..8bfab494 100644 --- a/includes/callback_node_access.inc +++ b/includes/callback_node_access.inc @@ -10,15 +10,9 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { /** - * Check whether this data-alter callback is applicable for a certain index. + * Overrides SearchApiAbstractAlterCallback::supportsIndex(). * * Returns TRUE only for indexes on nodes. - * - * @param SearchApiIndex $index - * The index to check for. - * - * @return boolean - * TRUE if the callback can run on the given index; FALSE otherwise. */ public function supportsIndex(SearchApiIndex $index) { // Currently only node access is supported. @@ -26,15 +20,9 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { } /** - * Declare the properties that are (or can be) added to items with this callback. + * Overrides SearchApiAbstractAlterCallback::propertyInfo(). * * 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( @@ -47,15 +35,7 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { } /** - * 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. + * {@inheritdoc} */ public function alterItems(array &$items) { static $account; @@ -65,30 +45,39 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { $account = drupal_anonymous_user(); } - foreach ($items as $nid => &$item) { + foreach ($items as $id => $item) { + $node = $this->getNode($item); // Check whether all users have access to the node. - if (!node_access('view', $item, $account)) { + if (!node_access('view', $node, $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)); + $result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->nid)); - // Store all grants together with it's realms in the item. + // Store all grants together with their 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"; + $items[$id]->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'); + $items[$id]->search_api_access_node = array('node_access__all'); } } } /** - * Submit callback for the configuration form. + * Retrieves the node related to a search item. + * + * In the default implementation for nodes, the item is already the node. + * Subclasses may override this to easily provide node access checks for + * items related to nodes. + */ + protected function getNode($item) { + return $item; + } + + /** + * Overrides SearchApiAbstractAlterCallback::configurationFormSubmit(). * * If the data alteration is being enabled, set "Published" and "Author" to * "indexed", because both are needed for the node access filter. diff --git a/search_api.info b/search_api.info index c03de9d0..5b65f830 100644 --- a/search_api.info +++ b/search_api.info @@ -11,6 +11,7 @@ files[] = includes/callback_add_hierarchy.inc files[] = includes/callback_add_url.inc files[] = includes/callback_add_viewed_entity.inc files[] = includes/callback_bundle_filter.inc +files[] = includes/callback_comment_access.inc files[] = includes/callback_language_control.inc files[] = includes/callback_node_access.inc files[] = includes/callback_node_status.inc diff --git a/search_api.module b/search_api.module index d5307e44..fe896788 100644 --- a/search_api.module +++ b/search_api.module @@ -1026,6 +1026,11 @@ function search_api_search_api_alter_callback_info() { 'description' => t('Add node access information to the index.'), 'class' => 'SearchApiAlterNodeAccess', ); + $callbacks['search_api_alter_comment_access'] = array( + 'name' => t('Access check'), + 'description' => t('Add node access information to the index.'), + 'class' => 'SearchApiAlterCommentAccess', + ); $callbacks['search_api_alter_node_status'] = array( 'name' => t('Exclude unpublished nodes'), 'description' => t('Exclude unpublished nodes from the index.'), @@ -1777,39 +1782,68 @@ function search_api_get_processors() { * The SearchApiQueryInterface object representing the search query. */ function search_api_search_api_query_alter(SearchApiQueryInterface $query) { + global $user; $index = $query->getIndex(); // Only add node access if the necessary fields are indexed in the index, and // unless disabled explicitly by the query. - $fields = $index->options['fields']; - if (!empty($fields['search_api_access_node']) && !empty($fields['status']) && !empty($fields['author']) && !$query->getOption('search_api_bypass_access')) { - $account = $query->getOption('search_api_access_account', $GLOBALS['user']); + $type = $index->getEntityType(); + if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]) && !$query->getOption('search_api_bypass_access')) { + $account = $query->getOption('search_api_access_account', $user); if (is_numeric($account)) { $account = user_load($account); } if (is_object($account)) { try { - _search_api_query_add_node_access($account, $query); + _search_api_query_add_node_access($account, $query, $type); } catch (SearchApiException $e) { watchdog_exception('search_api', $e); } } 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); + watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $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)) { + * Adds a node access filter to a search query, if applicable. + * + * @param object $account + * The user object, who searches. + * @param SearchApiQueryInterface $query + * The query to which a node access filter should be added, if applicable. + * @param string $type + * (optional) The type of search – either "node" or "comment". Defaults to + * "node". + * + * @throws SearchApiException + * If not all necessary fields are indexed on the index. + */ +function _search_api_query_add_node_access($account, SearchApiQueryInterface $query, $type = 'node') { + // Don't do anything if the user can access all content. + if (user_access('bypass node access', $account)) { + return; + } + + $is_comment = ($type == 'comment'); + + // Check whether the necessary fields are indexed. + $fields = $query->getIndex()->options['fields']; + $required = array('search_api_access_node', 'status'); + if (!$is_comment) { + $required[] = 'author'; + } + foreach ($required as $field) { + if (empty($fields[$field])) { + $vars['@field'] = $field; + $vars['@index'] = $query->getIndex()->name; + throw new SearchApiException(t('Required field @field not indexed on index @index. Could not perform access checks.', $vars)); + } + } + + // If the user cannot access content/comments at all, return no results. + if (!user_access('access content', $account) || ($is_comment && !user_access('access content', $account))) { // Simple hack for returning no results. $query->condition('status', 0); $query->condition('status', 1); @@ -1817,29 +1851,28 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu 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('author', $account->uid); - $query->filter($filter); - } - else { - $query->condition('status', NODE_PUBLISHED); - } - // Filter by node access grants. + // Filter by the "published" status. + $published = $is_comment ? COMMENT_PUBLISHED : NODE_PUBLISHED; + if (!$is_comment && user_access('view own unpublished content')) { $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'); + $filter->condition('status', $published); + $filter->condition('author', $account->uid); $query->filter($filter); } + else { + $query->condition('status', $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); } /** From 4ec13443efa55e0743efb2d527b6437a7d26a277 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 11 Nov 2013 09:22:01 +0100 Subject: [PATCH 044/278] Removed left-over CSS and applied some documentation fixes. --- search_api.admin.css | 29 ++++------------------------- search_api.admin.inc | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/search_api.admin.css b/search_api.admin.css index c8baf545..6af558b3 100644 --- a/search_api.admin.css +++ b/search_api.admin.css @@ -1,3 +1,7 @@ +/* + * OVERVIEW + */ + .search-api-overview td.search-api-status { text-align: center; } @@ -6,31 +10,6 @@ vertical-align: top; } -.search-api-edit-menu { - position: relative; - vertical-align: top; -} - -.search-api-edit-menu .ctools-dropbutton-processed { - position: absolute; - top: 0; - right: 0; - left: 0; - border-radius: 11px; - /* We need some background color, or the popup will look weird. - * Since we can't know what theme we are on, we just guess it's "Seven". - */ - background-color: #FFF; -} - -.search-api-edit-menu .ctools-dropbutton-processed.open { - left: auto; -} - -tr.even .search-api-edit-menu .ctools-dropbutton-processed { - background-color: #F3F4EE; -} - /* * DROPBUTTONS * diff --git a/search_api.admin.inc b/search_api.admin.inc index 0630818b..8161ff3c 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1,5 +1,10 @@ Date: Wed, 13 Nov 2013 10:54:06 +0100 Subject: [PATCH 045/278] Some doc style fixes. --- contrib/search_api_views/search_api_views.install | 11 +++++++++-- includes/processor_highlight.inc | 4 ++-- search_api.module | 8 +++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/contrib/search_api_views/search_api_views.install b/contrib/search_api_views/search_api_views.install index 804d3079..254e8409 100644 --- a/contrib/search_api_views/search_api_views.install +++ b/contrib/search_api_views/search_api_views.install @@ -66,8 +66,15 @@ function search_api_views_update_7101() { /** * Helper function for replacing field identifiers. * - * @return - * TRUE iff the identifier was changed. + * @param $field + * Some data to be searched for field names that should be altered. Passed by + * reference. + * @param array $fields + * An array mapping Search API field identifiers (as previously used by Views) + * to the new, sanitized Views field identifiers. + * + * @return bool + * TRUE if any data was changed, FALSE otherwise. */ function _search_api_views_update_7101_helper(&$field, array $fields) { if (is_array($field)) { diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 8ad8d02e..a7ed4dbe 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -150,8 +150,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { /** * Retrieves the fulltext data of a result. * - * @param array $result - * All results returned in the search. + * @param array $results + * All results returned in the search, by reference. * @param int|string $i * The index in the results array of the result whose data should be * returned. diff --git a/search_api.module b/search_api.module index fe896788..51d67391 100644 --- a/search_api.module +++ b/search_api.module @@ -2001,20 +2001,18 @@ function search_api_index_update_datasource(SearchApiIndex $index, $table, $colu } /** - * Utility function for extracting specific fields from an EntityMetadataWrapper - * object. + * Extracts specific field values from an EntityMetadataWrapper object. * * @param EntityMetadataWrapper $wrapper * The wrapper from which to extract fields. * @param array $fields * The fields to extract, as stored in an index. I.e., the array keys are - * field names, the values are arrays with the keys "name", "type", "boost" - * and "indexed" (although only "type" is used by this function). + * field names, the values are arrays with at least a "type" key present. * @param array $value_options * An array of options that should be passed to the * EntityMetadataWrapper::value() method (see there). * - * @return + * @return array * The $fields array with additional "value" and "original_type" keys set. */ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields, array $value_options = array()) { From dd5c6787e2e33a810dd9980d61b2ca392f05ab9e Mon Sep 17 00:00:00 2001 From: damz Date: Wed, 13 Nov 2013 11:47:41 +0100 Subject: [PATCH 046/278] Issue #1832334 by Damien Tournoud, drunken monkey: Fixed performance issues of Views options filter handler for huge options lists. --- CHANGELOG.txt | 2 + .../includes/handler_filter_options.inc | 117 ++++++++++++++---- .../search_api_views.views.inc | 4 +- 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0281dacc..c17bf835 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1832334 by Damien Tournoud, drunken monkey: Fixed performance issues of + Views options filter handler for huge options lists. - #2118589 by mxr576, drunken monkey: Added node access for comment indexes. - #1961120 by drunken monkey: Fixed Views handling of short fulltext keywords. - #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters". diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc index 92545445..c63c07e7 100644 --- a/contrib/search_api_views/includes/handler_filter_options.inc +++ b/contrib/search_api_views/includes/handler_filter_options.inc @@ -1,16 +1,82 @@ query) { + $index = $this->query->getIndex(); + } + elseif (substr($this->view->base_table, 0, 17) == 'search_api_index_') { + $index = search_api_index_load(substr($this->view->base_table, 17)); + } + else { + return NULL; + } + $wrapper = $index->entityWrapper(NULL, TRUE); + $parts = explode(':', $this->real_field); + foreach ($parts as $i => $part) { + if (!isset($wrapper->$part)) { + return NULL; + } + $wrapper = $wrapper->$part; + $info = $wrapper->info(); + if ($i < count($parts) - 1) { + // Unwrap lists. + $level = search_api_list_nesting_level($info['type']); + for ($j = 0; $j < $level; ++$j) { + $wrapper = $wrapper[0]; + } + } + } + + return $wrapper; + } + + /** + * Fills the value_options property with all possible options. + */ + protected function get_value_options() { + if (isset($this->value_options)) { + return; + } + + $wrapper = $this->get_wrapper(); + if ($wrapper) { + $this->value_options = $wrapper->optionsList('view'); + } + else { + $this->value_options = array(); + } + } + /** * Provide a list of options for the operator form. */ @@ -63,13 +129,12 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { * Reduce the options according to the selection. */ protected function reduce_value_options() { - $options = array(); - foreach ($this->definition['options'] as $id => $option) { - if (isset($this->options['value'][$id])) { - $options[$id] = $option; + foreach ($this->value_options as $id => $option) { + if (!isset($this->options['value'][$id])) { + unset($this->value_options[$id]); } } - return $options; + return $this->value_options; } /** @@ -92,19 +157,20 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { * Provide a form for setting options. */ public function value_form(&$form, &$form_state) { - $options = array(); + $this->get_value_options(); if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) { - $options += $this->reduce_value_options($form_state); + $options = $this->reduce_value_options(); } else { - $options += $this->definition['options']; + $options = $this->value_options; } + $form['value'] = array( '#type' => $this->value_form_type, '#title' => empty($form_state['exposed']) ? t('Value') : '', '#options' => $options, '#multiple' => TRUE, - '#size' => min(4, count($this->definition['options'])), + '#size' => min(4, count($options)), '#default_value' => is_array($this->value) ? $this->value : array(), ); @@ -149,8 +215,9 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { $values = ''; // Remove every element which is not known. + $this->get_value_options(); foreach ($this->value as $i => $value) { - if (!isset($this->definition['options'][$value])) { + if (!isset($this->value_options[$value])) { unset($this->value[$i]); } } @@ -171,7 +238,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { } // If there is only a single value, use just the plain operator, = or <>. $operator = check_plain($operator); - $values = check_plain($this->definition['options'][reset($this->value)]); + $values = check_plain($this->value_options[reset($this->value)]); } else { foreach ($this->value as $value) { @@ -182,7 +249,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { $values .= '…'; break; } - $values .= check_plain($this->definition['options'][$value]); + $values .= check_plain($this->value_options[$value]); } } @@ -207,28 +274,24 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { $this->value = reset($this->value); } - // Determine operator and conjunction. + // Determine operator and conjunction. The defaults are already right for + // "all of". + $operator = '='; + $conjunction = 'AND'; switch ($this->operator) { case '=': - $operator = '='; $conjunction = 'OR'; break; - case 'all of': - $operator = '='; - $conjunction = 'AND'; - break; - case '<>': $operator = '<>'; - $conjunction = 'AND'; break; } // If the value is an empty array, we either want no filter at all (for - // "is none of", or want to find only items with no value for the field. + // "is none of"), or want to find only items with no value for the field. if ($this->value === array()) { - if ($this->operator != '<>') { + if ($operator != '<>') { $this->query->condition($this->real_field, NULL, '=', $this->options['group']); } return; diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index e9c9d23f..90f1436f 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -188,9 +188,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper return; } - if ($options = $wrapper->optionsList('view')) { + $info = $wrapper->info(); + if (isset($info['options list']) && is_callable($info['options list'])) { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions'; - $table[$id]['filter']['options'] = $options; $table[$id]['filter']['multi-valued'] = search_api_is_list_type($type); } elseif ($inner_type == 'boolean') { From 3335094e0d9cc31a6b656e4ad6c1f9c0da14c881 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 13 Nov 2013 16:50:13 +0100 Subject: [PATCH 047/278] Some doc comment fixes for includes/query.inc. --- includes/query.inc | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/includes/query.inc b/includes/query.inc index d2961dc8..521e0e19 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -318,7 +318,8 @@ interface SearchApiQueryInterface { * @param mixed $value * The new value of the option. * - * @return The option's previous value. + * @return mixed + * The option's previous value. */ public function setOption($name, $value); @@ -341,12 +342,21 @@ interface SearchApiQueryInterface { class SearchApiQuery implements SearchApiQueryInterface { /** - * The index. + * The index this query will use. * * @var SearchApiIndex */ protected $index; + /** + * The index's machine name. + * + * used during serialization to avoid serializing the whole index object. + * + * @var string + */ + protected $index_id; + /** * The search keys. If NULL, this will be a filter-only search. * @@ -616,6 +626,9 @@ class SearchApiQuery implements SearchApiQueryInterface { * * @param array $languages * The languages for which results should be returned. + * + * @throws SearchApiException + * If there was a logical error in the combination of filters and languages. */ protected function addLanguages(array $languages) { if (array_search(LANGUAGE_NONE, $languages) === FALSE) { From 5246b64c6633259c6edc07fe802265869587df60 Mon Sep 17 00:00:00 2001 From: dww Date: Thu, 14 Nov 2013 08:36:59 +0100 Subject: [PATCH 048/278] Issue #2135255 by dww: Fixed missing pager on first page of search results. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c17bf835..bc4a7b8b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2135255 by dww: Fixed missing pager on first page of search results. - #1832334 by Damien Tournoud, drunken monkey: Fixed performance issues of Views options filter handler for huge options lists. - #2118589 by mxr576, drunken monkey: Added node access for comment indexes. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 85bdb9c0..69be5c3b 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -322,7 +322,7 @@ class SearchApiViewsQuery extends views_plugin_query { $view->execute_time = microtime(TRUE) - $start; // Trigger pager post_execute(). - $this->pager->post_execute($view->results); + $this->pager->post_execute($view->result); } catch (Exception $e) { $this->errors[] = $e->getMessage(); From 3c886fe2671c2ed8870457f9eef122f68bff1700 Mon Sep 17 00:00:00 2001 From: damz Date: Thu, 14 Nov 2013 10:01:50 +0100 Subject: [PATCH 049/278] Issue #1390598 by Damien Tournoud, drunken monkey: Added the concept of query filter tags. --- CHANGELOG.txt | 2 + .../plugins/facetapi/query_type_term.inc | 21 ++++---- contrib/search_api_views/includes/query.inc | 4 +- includes/query.inc | 54 ++++++++++++++++--- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bc4a7b8b..76decd93 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1390598 by Damien Tournoud, drunken monkey: Added the concept of query filter + tags. - #2135255 by dww: Fixed missing pager on first page of search results. - #1832334 by Damien Tournoud, drunken monkey: Fixed performance issues of Views options filter handler for huge options lists. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 96ad37b6..33dacc16 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -40,24 +40,27 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy return; } - if (FACETAPI_OPERATOR_OR == $operator) { - // If we're dealing with an OR facet, we need to use a nested filter. - $facet_filter = $query->createFilter('OR'); + // Create the facet filter, and add a tag to it so that it can be easily + // identified down the line by services when they need to exclude facets. + if ($operator == FACETAPI_OPERATOR_AND) { + $conjunction = 'AND'; + } + elseif ($operator == FACETAPI_OPERATOR_OR) { + $conjunction = 'OR'; } else { - // Otherwise we set the conditions directly on the query. - $facet_filter = $query; + throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator))); } + $tags = array('facet:' . $this->facet['field']); + $facet_filter = $query->createFilter($conjunction, $tags); foreach ($active as $filter => $filter_array) { $field = $this->facet['field']; $this->addFacetFilter($facet_filter, $field, $filter); } - // For OR facets, we now have to add the filter to the query. - if (FACETAPI_OPERATOR_OR == $operator) { - $query->filter($facet_filter); - } + // Now add the filter to the query. + $query->filter($facet_filter); } /** diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 69be5c3b..05e9d3ba 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -545,9 +545,9 @@ class SearchApiViewsQuery extends views_plugin_query { // Query interface methods (proxy to $this->query) // - public function createFilter($conjunction = 'AND') { + public function createFilter($conjunction = 'AND', $tags = array()) { if (!$this->errors) { - return $this->query->createFilter($conjunction); + return $this->query->createFilter($conjunction, $tags); } } diff --git a/includes/query.inc b/includes/query.inc index 521e0e19..0bceb4c9 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -62,11 +62,15 @@ interface SearchApiQueryInterface { * * @param string $conjunction * The conjunction to use for the filter - either 'AND' or 'OR'. + * @param $tags + * (Optional) An arbitrary set of tags. Can be used to identify this filter + * down the line if necessary. This is primarily used by the facet system + * to support OR facet queries. * * @return SearchApiQueryFilterInterface * A filter object that is set to use the specified conjunction. */ - public function createFilter($conjunction = 'AND'); + public function createFilter($conjunction = 'AND', $tags = array()); /** * Sets the keys to search for. @@ -513,9 +517,9 @@ class SearchApiQuery implements SearchApiQueryInterface { /** * {@inheritdoc} */ - public function createFilter($conjunction = 'AND') { + public function createFilter($conjunction = 'AND', $tags = array()) { $filter_class = $this->options['filter class']; - return new $filter_class($conjunction); + return new $filter_class($conjunction, $tags); } /** @@ -803,9 +807,13 @@ interface SearchApiQueryFilterInterface { * Constructs a new filter that uses the specified conjunction. * * @param string $conjunction - * The conjunction to use for this filter - either 'AND' or 'OR'. + * (optional) The conjunction to use for this filter - either 'AND' or 'OR'. + * @param array $tags + * (optional) An arbitrary set of tags. Can be used to identify this filter + * down the line if necessary. This is primarily used by the facet system + * to support OR facet queries. */ - public function __construct($conjunction = 'AND'); + public function __construct($conjunction = 'AND', array $tags = array()); /** * Sets this filter's conjunction. @@ -869,6 +877,25 @@ interface SearchApiQueryFilterInterface { */ public function &getFilters(); + /** + * Checks whether a certain tag was set on this filter. + * + * @param string $tag + * A tag to check for. + * + * @return bool + * TRUE if the tag was set for this filter, FALSE otherwise. + */ + public function hasTag($tag); + + /** + * Retrieves the tags set on this filter. + * + * @return array + * The tags associated with this filter, as both the array keys and values. + */ + public function &getTags(); + } /** @@ -896,9 +923,10 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { /** * {@inheritdoc} */ - public function __construct($conjunction = 'AND') { + public function __construct($conjunction = 'AND', array $tags = array()) { $this->setConjunction($conjunction); $this->filters = array(); + $this->tags = drupal_map_assoc($tags); } /** @@ -939,4 +967,18 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { return $this->filters; } + /** + * {@inheritdoc} + */ + public function hasTag($tag) { + return isset($this->tags[$tag]); + } + + /** + * {@inheritdoc} + */ + public function &getTags() { + return $this->tags; + } + } From e456c9ec18977dafc792e9eb50fd6ef25089ed02 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 14 Nov 2013 11:32:47 +0100 Subject: [PATCH 050/278] Some doc comment fixes. --- includes/callback_bundle_filter.inc | 26 ++- includes/datasource.inc | 253 ++++++++++------------------ includes/datasource_entity.inc | 80 +-------- 3 files changed, 122 insertions(+), 237 deletions(-) diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index e1072b6b..576fa608 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -1,15 +1,25 @@ getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); } + /** + * {@inheritdoc} + */ public function alterItems(array &$items) { $info = entity_get_info($this->index->getEntityType()); if (self::hasBundles($info) && isset($this->options['bundles'])) { @@ -24,6 +34,9 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { } } + /** + * {@inheritdoc} + */ public function configurationForm() { $info = entity_get_info($this->index->getEntityType()); if (self::hasBundles($info)) { @@ -62,8 +75,13 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { } /** - * Helper method for figuring out if the entities with the given entity info - * can be filtered by bundle. + * Determines whether a certain entity type has any bundles. + * + * @param array $entity_info + * The entity type's entity_get_info() array. + * + * @return bool + * TRUE if the entity type has bundles, FASLE otherwise. */ protected static function hasBundles(array $entity_info) { return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']); diff --git a/includes/datasource.inc b/includes/datasource.inc index a515d66d..e325644a 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -18,46 +18,49 @@ * aware that indexes' numerical IDs can change due to feature reverts. It is * therefore recommended to use search_api_index_update_datasource(), or similar * code, in a hook_search_api_index_update() implementation. - * - * All methods of the data source may throw exceptions of type - * SearchApiDataSourceException if any exception or error state is encountered. */ interface SearchApiDataSourceControllerInterface { /** - * Constructor for a data source controller. + * Constructs a new data source controller. * - * @param $type + * @param string $type * The item type for which this controller is created. */ public function __construct($type); /** - * Return information on the ID field for this controller's type. + * Returns information on the ID field for this controller's type. * * @return array * An associative array containing the following keys: * - key: The property key for the ID field, as used in the item wrapper. * - type: The type of the ID field. Has to be one of the types from * search_api_field_types(). List types ("list<*>") are not allowed. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getIdFieldInfo(); /** - * Load items of the type of this data source controller. + * Loads items of the type of this data source controller. * * @param array $ids * The IDs of the items to laod. * * @return array * The loaded items, keyed by ID. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function loadItems(array $ids); /** - * Get a metadata wrapper for the item type of this data source controller. + * Creates a metadata wrapper for this datasource controller's type. * - * @param $item + * @param mixed $item * Unless NULL, an item of the item type for this controller to be wrapped. * @param array $info * Optionally, additional information that should be used for creating the @@ -67,47 +70,59 @@ interface SearchApiDataSourceControllerInterface { * A wrapper for the item type of this data source controller, according to * the info array, and optionally loaded with the given data. * + * @throws SearchApiDataSourceException + * If any error state was encountered. + * * @see entity_metadata_wrapper() */ public function getMetadataWrapper($item = NULL, array $info = array()); /** - * Get the unique ID of an item. + * Retrieves the unique ID of an item. * - * @param $item + * @param mixed $item * An item of this controller's type. * - * @return + * @return mixed * Either the unique ID of the item, or NULL if none is available. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getItemId($item); /** - * Get a human-readable label for an item. + * Retrieves a human-readable label for an item. * - * @param $item + * @param mixed $item * An item of this controller's type. * - * @return + * @return string|null * Either a human-readable label for the item, or NULL if none is available. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getItemLabel($item); /** - * Get a URL at which the item can be viewed on the web. + * Retrieves a URL at which the item can be viewed on the web. * - * @param $item + * @param mixed $item * An item of this controller's type. * - * @return + * @return array|null * Either an array containing the 'path' and 'options' keys used to build * the URL of the item, and matching the signature of url(), or NULL if the * item has no URL of its own. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getItemUrl($item); /** - * Initialize tracking of the index status of items for the given indexes. + * Initializes tracking of the index status of items for the given indexes. * * All currently known items of this data source's type should be inserted * into the tracking table for the given indexes, with status "changed". If @@ -118,12 +133,12 @@ interface SearchApiDataSourceControllerInterface { * The SearchApiIndex objects for which item tracking should be initialized. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If any error state was encountered. */ public function startTracking(array $indexes); /** - * Stop tracking of the index status of items for the given indexes. + * Stops tracking of the index status of items for the given indexes. * * The tracking tables of the given indexes should be completely cleared. * @@ -131,12 +146,12 @@ interface SearchApiDataSourceControllerInterface { * The SearchApiIndex objects for which item tracking should be stopped. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If any error state was encountered. */ public function stopTracking(array $indexes); /** - * Start tracking the index status for the given items on the given indexes. + * Starts tracking the index status for the given items on the given indexes. * * @param array $item_ids * The IDs of new items to track. @@ -144,45 +159,45 @@ interface SearchApiDataSourceControllerInterface { * The indexes for which items should be tracked. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If any error state was encountered. */ public function trackItemInsert(array $item_ids, array $indexes); /** - * Set the tracking status of the given items to "changed"/"dirty". + * Sets the tracking status of the given items to "changed"/"dirty". * * Unless $dequeue is set to TRUE, this operation is ignored for items whose * status is not "indexed". * - * @param $item_ids + * @param array|false $item_ids * Either an array with the IDs of the changed items. Or FALSE to mark all * items as changed for the given indexes. * @param array $indexes * The indexes for which the change should be tracked. - * @param $dequeue + * @param bool $dequeue * (deprecated) If set to TRUE, also change the status of queued items. * The concept of queued items will be removed in the Drupal 8 version of * this module. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If any error state was encountered. */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE); /** - * Set the tracking status of the given items to "queued". + * Sets the tracking status of the given items to "queued". * * Queued items are not marked as "dirty" even when they are changed, and they * are not returned by the getChangedItems() method. * - * @param $item_ids + * @param array|false $item_ids * Either an array with the IDs of the queued items. Or FALSE to mark all * items as queued for the given indexes. * @param SearchApiIndex $index * The index for which the items were queued. * * @throws SearchApiDataSourceException - * If the index doesn't use the same item type as this controller. + * If any error state was encountered. * * @deprecated * As of Search API 1.10, the cron queue is not used for indexing anymore, @@ -192,7 +207,7 @@ interface SearchApiDataSourceControllerInterface { public function trackItemQueued($item_ids, SearchApiIndex $index); /** - * Set the tracking status of the given items to "indexed". + * Sets the tracking status of the given items to "indexed". * * @param array $item_ids * The IDs of the indexed items. @@ -200,12 +215,12 @@ interface SearchApiDataSourceControllerInterface { * The index on which the items were indexed. * * @throws SearchApiDataSourceException - * If the index doesn't use the same item type as this controller. + * If any error state was encountered. */ public function trackItemIndexed(array $item_ids, SearchApiIndex $index); /** - * Stop tracking the index status for the given items on the given indexes. + * Stops tracking the index status for the given items on the given indexes. * * @param array $item_ids * The IDs of the removed items. @@ -213,12 +228,12 @@ interface SearchApiDataSourceControllerInterface { * The indexes for which the deletions should be tracked. * * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * If any error state was encountered. */ public function trackItemDelete(array $item_ids, array $indexes); /** - * Get a list of items that need to be indexed. + * Retrieves a list of items that need to be indexed. * * If possible, completely unindexed items should be returned before items * that were indexed but later changed. Also, items that were changed longer @@ -226,16 +241,19 @@ interface SearchApiDataSourceControllerInterface { * * @param SearchApiIndex $index * The index for which changed items should be returned. - * @param $limit + * @param int $limit * The maximum number of items to return. Negative values mean "unlimited". * * @return array * The IDs of items that need to be indexed for the given index. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getChangedItems(SearchApiIndex $index, $limit = -1); /** - * Get information on how many items have been indexed for a certain index. + * Retrieves information on how many items have been indexed for a certain index. * * @param SearchApiIndex $index * The index whose index status should be returned. @@ -247,22 +265,26 @@ interface SearchApiDataSourceControllerInterface { * index. * * @throws SearchApiDataSourceException - * If the index doesn't use the same item type as this controller. + * If any error state was encountered. */ public function getIndexStatus(SearchApiIndex $index); /** - * Get the entity type of items from this datasource. + * Retrieves the entity type of items from this datasource. * * @return string|null * An entity type string if the items provided by this datasource are * entities; NULL otherwise. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ public function getEntityType(); + } /** - * Default base class for the SearchApiDataSourceControllerInterface. + * Provides a default base class for datasource controllers. * * Contains default implementations for a number of methods which will be * similar for most data sources. Concrete data sources can decide to extend @@ -337,10 +359,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou protected $changedColumn = 'changed'; /** - * Constructor for a data source controller. - * - * @param $type - * The item type for which this controller is created. + * {@inheritdoc} */ public function __construct($type) { $this->type = $type; @@ -352,30 +371,14 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get the entity type of items from this datasource. - * - * @return string|null - * An entity type string if the items provided by this datasource are - * entities; NULL otherwise. + * {@inheritdoc} */ public function getEntityType() { return $this->entityType; } /** - * Get a metadata wrapper for the item type of this data source controller. - * - * @param $item - * Unless NULL, an item of the item type for this controller to be wrapped. - * @param array $info - * Optionally, additional information that should be used for creating the - * wrapper. Uses the same format as entity_metadata_wrapper(). - * - * @return EntityMetadataWrapper - * A wrapper for the item type of this data source controller, according to - * the info array, and optionally loaded with the given data. - * - * @see entity_metadata_wrapper() + * {@inheritdoc} */ public function getMetadataWrapper($item = NULL, array $info = array()) { $info += $this->getPropertyInfo(); @@ -383,7 +386,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get the property info for this item type. + * Retrieves the property info for this item type. * * This is a helper method for getMetadataWrapper() that can be used by * subclasses to specify the property information to use when creating a @@ -420,6 +423,9 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * @return array * Property information as specified by entity_metadata_wrapper(). * + * @throws SearchApiDataSourceException + * If any error state was encountered. + * * @see getMetadataWrapper() * @see hook_entity_property_info() */ @@ -432,13 +438,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get the unique ID of an item. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either the unique ID of the item, or NULL if none is available. + * {@inheritdoc} */ public function getItemId($item) { $id_info = $this->getIdFieldInfo(); @@ -452,13 +452,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get a human-readable label for an item. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either a human-readable label for the item, or NULL if none is available. + * {@inheritdoc} */ public function getItemLabel($item) { $label = $this->getMetadataWrapper($item)->label(); @@ -466,33 +460,14 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get a URL at which the item can be viewed on the web. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either an array containing the 'path' and 'options' keys used to build - * the URL of the item, and matching the signature of url(), or NULL if the - * item has no URL of its own. + * {@inheritdoc} */ public function getItemUrl($item) { return NULL; } /** - * Initialize tracking of the index status of items for the given indexes. - * - * All currently known items of this data source's type should be inserted - * into the tracking table for the given indexes, with status "changed". If - * items were already present, these should also be set to "changed" and not - * be inserted again. - * - * @param array $indexes - * The SearchApiIndex objects for which item tracking should be initialized. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function startTracking(array $indexes) { if (!$this->table) { @@ -506,27 +481,23 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Helper method that can be used by subclasses instead of implementing startTracking(). - * * Returns the IDs of all items that are known for this controller's type. * + * Helper method that can be used by subclasses instead of implementing + * startTracking(). + * * @return array * An array containing all item IDs for this type. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. */ protected function getAllItemIds() { throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type))); } /** - * Stop tracking of the index status of items for the given indexes. - * - * The tracking tables of the given indexes should be completely cleared. - * - * @param array $indexes - * The SearchApiIndex objects for which item tracking should be stopped. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function stopTracking(array $indexes) { if (!$this->table) { @@ -543,15 +514,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Start tracking the index status for the given items on the given indexes. - * - * @param array $item_ids - * The IDs of new items to track. - * @param array $indexes - * The indexes for which items should be tracked. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function trackItemInsert(array $item_ids, array $indexes) { if (!$this->table) { @@ -621,15 +584,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Set the tracking status of the given items to "indexed". - * - * @param array $item_ids - * The IDs of the indexed items. - * @param SearchApiIndex $indexes - * The index on which the items were indexed. - * - * @throws SearchApiDataSourceException - * If the index doesn't use the same item type as this controller. + * {@inheritdoc} */ public function trackItemIndexed(array $item_ids, SearchApiIndex $index) { if (!$this->table) { @@ -646,15 +601,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Stop tracking the index status for the given items on the given indexes. - * - * @param array $item_ids - * The IDs of the removed items. - * @param array $indexes - * The indexes for which the deletions should be tracked. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function trackItemDelete(array $item_ids, array $indexes) { if (!$this->table) { @@ -672,19 +619,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get a list of items that need to be indexed. - * - * If possible, completely unindexed items should be returned before items - * that were indexed but later changed. Also, items that were changed longer - * ago should be favored. - * - * @param SearchApiIndex $index - * The index for which changed items should be returned. - * @param $limit - * The maximum number of items to return. Negative values mean "unlimited". - * - * @return array - * The IDs of items that need to be indexed for the given index. + * {@inheritdoc} */ public function getChangedItems(SearchApiIndex $index, $limit = -1) { if ($limit == 0) { @@ -703,16 +638,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Get information on how many items have been indexed for a certain index. - * - * @param SearchApiIndex $index - * The index whose index status should be returned. - * - * @return array - * An associative array containing two keys (in this order): - * - indexed: The number of items already indexed in their latest version. - * - total: The total number of items that have to be indexed for this - * index. + * {@inheritdoc} */ public function getIndexStatus(SearchApiIndex $index) { if (!$this->table) { @@ -734,13 +660,16 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Helper method for ensuring that an index uses the same item type as this controller. + * Checks whether the given index is valid for this datasource controller. + * + * Helper method used by various methods in this class. By default only checks + * whether the types match. * * @param SearchApiIndex $index * The index to check. * * @throws SearchApiDataSourceException - * If the index doesn't use the same type as this controller. + * If the index doesn't fit to this datasource controller. */ protected function checkIndex(SearchApiIndex $index) { if ($index->item_type != $this->type) { diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 6f15ec54..836f57e3 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -6,18 +6,12 @@ */ /** - * Data source for all entities known to the Entity API. + * Represents a datasource for all entities known to the Entity API. */ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController { /** - * Return information on the ID field for this controller's type. - * - * @return array - * An associative array containing the following keys: - * - key: The property key for the ID field, as used in the item wrapper. - * - type: The type of the ID field. Has to be one of the types from - * search_api_field_types(). List types ("list<*>") are not allowed. + * {@inheritdoc} */ public function getIdFieldInfo() { $info = entity_get_info($this->entityType); @@ -43,13 +37,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Load items of the type of this data source controller. - * - * @param array $ids - * The IDs of the items to laod. - * - * @return array - * The loaded items, keyed by ID. + * {@inheritdoc} */ public function loadItems(array $ids) { $items = entity_load($this->entityType, $ids); @@ -65,32 +53,14 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Get a metadata wrapper for the item type of this data source controller. - * - * @param $item - * Unless NULL, an item of the item type for this controller to be wrapped. - * @param array $info - * Optionally, additional information that should be used for creating the - * wrapper. Uses the same format as entity_metadata_wrapper(). - * - * @return EntityMetadataWrapper - * A wrapper for the item type of this data source controller, according to - * the info array, and optionally loaded with the given data. - * - * @see entity_metadata_wrapper() + * {@inheritdoc} */ public function getMetadataWrapper($item = NULL, array $info = array()) { return entity_metadata_wrapper($this->entityType, $item, $info); } /** - * Get the unique ID of an item. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either the unique ID of the item, or NULL if none is available. + * {@inheritdoc} */ public function getItemId($item) { $id = entity_id($this->entityType, $item); @@ -98,13 +68,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Get a human-readable label for an item. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either a human-readable label for the item, or NULL if none is available. + * {@inheritdoc} */ public function getItemLabel($item) { $label = entity_label($this->entityType, $item); @@ -112,15 +76,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Get a URL at which the item can be viewed on the web. - * - * @param $item - * An item of this controller's type. - * - * @return - * Either an array containing the 'path' and 'options' keys used to build - * the URL of the item, and matching the signature of url(), or NULL if the - * item has no URL of its own. + * {@inheritdoc} */ public function getItemUrl($item) { if ($this->entityType == 'file') { @@ -137,18 +93,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Initialize tracking of the index status of items for the given indexes. - * - * All currently known items of this data source's type should be inserted - * into the tracking table for the given indexes, with status "changed". If - * items were already present, these should also be set to "changed" and not - * be inserted again. - * - * @param array $indexes - * The SearchApiIndex objects for which item tracking should be initialized. - * - * @throws SearchApiDataSourceException - * If any of the indexes doesn't use the same item type as this controller. + * {@inheritdoc} */ public function startTracking(array $indexes) { if (!$this->table) { @@ -190,14 +135,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } /** - * Helper method that can be used by subclasses instead of implementing startTracking(). - * - * Returns the IDs of all items that are known for this controller's type. - * - * Will be used when the entity type doesn't specify a "base table". - * - * @return array - * An array containing all item IDs for this type. + * {@inheritdoc} */ protected function getAllItemIds() { return array_keys(entity_load($this->entityType)); From 1a10772868099cbfe9e507a57b38e0783e5b5fac Mon Sep 17 00:00:00 2001 From: drumm Date: Fri, 15 Nov 2013 10:32:24 +0100 Subject: [PATCH 051/278] Issue #2135363 by drumm, drunken monkey: Added support for Views' use_count_query() method. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 11 +++++++---- includes/query.inc | 8 +++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 76decd93..e800f172 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2135363 by drumm, drunken monkey: Added support for Views' use_count_query() + method. - #1390598 by Damien Tournoud, drunken monkey: Added the concept of query filter tags. - #2135255 by dww: Fixed missing pager on first page of search results. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 05e9d3ba..c029feb8 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -241,6 +241,7 @@ class SearchApiViewsQuery extends views_plugin_query { // Initialize the pager and let it modify the query to add limits. $view->init_pager(); $this->pager->query(); + $this->query->setOption('skip result count', $this->pager->use_count_query()); // Views passes sometimes NULL and sometimes the integer 0 for "All" in a // pager. If set to 0 items, a string "0" is passed. Therefore, we unset @@ -308,11 +309,13 @@ class SearchApiViewsQuery extends views_plugin_query { $this->search_api_results = $results; // Store the results. - $this->pager->total_items = $view->total_rows = $results['result count']; - if (!empty($this->pager->options['offset'])) { - $this->pager->total_items -= $this->pager->options['offset']; + if ($this->pager->use_count_query()) { + $this->pager->total_items = $view->total_rows = $results['result count']; + if (!empty($this->pager->options['offset'])) { + $this->pager->total_items -= $this->pager->options['offset']; + } + $this->pager->update_page_info(); } - $this->pager->update_page_info(); $view->result = array(); if (!empty($results['results'])) { $this->addResults($results['results'], $view); diff --git a/includes/query.inc b/includes/query.inc index 0bceb4c9..41e2d57d 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -33,6 +33,10 @@ 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. + * - 'skip result count': If present and set to TRUE, the result's + * "result count" key will not be needed. Service classes can check for + * this option to possibly avoid executing expensive operations to compute + * the result count in cases where it is not needed. * - 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 @@ -179,7 +183,9 @@ interface SearchApiQueryInterface { * An associative array containing the search results. The following keys * are standardized: * - 'result count': The overall number of results for this query, without - * range restrictions. Might be approximated, for large numbers. + * range restrictions. Might be approximated, for large numbers, or + * skipped entirely if the "skip result count" option was set on this + * query. * - results: An array of results, ordered as specified. The array keys are * the items' IDs, values are arrays containing the following keys: * - id: The item's ID. From ffb1ae2b614a063f1f0ff903d7048a30c395adeb Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 15 Nov 2013 10:33:29 +0100 Subject: [PATCH 052/278] Issue #1551302 by drunken monkey: Fixed the server tasks system. --- CHANGELOG.txt | 1 + includes/index_entity.inc | 55 ++----- includes/server_entity.inc | 156 +++++++++++++++++- includes/service.inc | 66 ++++++-- search_api.admin.inc | 42 +++-- search_api.install | 142 +++++++++++++++- search_api.module | 305 ++++++++++++++++++++++++----------- search_api.test | 127 ++++++++++++++- tests/search_api_test.module | 50 +++++- 9 files changed, 750 insertions(+), 194 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e800f172..e3ee0d63 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1551302 by drunken monkey: Fixed the server tasks system. - #2135363 by drumm, drunken monkey: Added support for Views' use_count_query() method. - #1390598 by Damien Tournoud, drunken monkey: Added the concept of query filter diff --git a/includes/index_entity.inc b/includes/index_entity.inc index dc456666..8381942a 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -178,18 +178,9 @@ class SearchApiIndex extends Entity { if ($this->enabled) { $this->queueItems(); } - $server = $this->server(); - if ($server) { + if ($server = $this->server()) { // Tell the server about the new index. - if ($server->enabled) { - $server->addIndex($this); - } - else { - $tasks = variable_get('search_api_tasks', array()); - // When we add or remove an index, we can ignore all other tasks. - $tasks[$server->machine_name][$this->machine_name] = array('add'); - variable_set('search_api_tasks', $tasks); - } + $server->addIndex($this); } } @@ -198,18 +189,7 @@ class SearchApiIndex extends Entity { */ public function postDelete() { if ($server = $this->server()) { - if ($server->enabled) { - $server->removeIndex($this); - } - // Once the index is deleted, servers won't be able to tell whether it was - // read-only. Therefore, we prefer to err on the safe side and don't call - // the server method at all if the index is read-only and the server - // currently disabled. - elseif (empty($this->read_only)) { - $tasks = variable_get('search_api_tasks', array()); - $tasks[$server->machine_name][$this->machine_name] = array('remove'); - variable_set('search_api_tasks', $tasks); - } + $server->removeIndex($this); } // Stop tracking entities for indexing. @@ -233,10 +213,11 @@ class SearchApiIndex extends Entity { } /** - * Saves this index to the database, either creating a new record or updating - * an existing one. + * Saves this index to the database. * - * @return + * Either creates a new record or updates the existing one with the same ID. + * + * @return int|false * Failure to save the index will return FALSE. Otherwise, SAVED_NEW or * SAVED_UPDATED is returned depending on the operation performed. $this->id * will be set if a new index was inserted. @@ -252,6 +233,7 @@ class SearchApiIndex extends Entity { // This will also throw an exception if the server doesn't exist – which is good. elseif (!$this->server(TRUE)->enabled) { $this->enabled = FALSE; + $this->server = NULL; } return parent::save(); @@ -266,7 +248,7 @@ class SearchApiIndex extends Entity { * @param array $fields * The new field values. * - * @return + * @return int|false * SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had * the specified values. */ @@ -295,7 +277,7 @@ class SearchApiIndex extends Entity { /** * Schedules this search index for re-indexing. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ public function reindex() { @@ -310,7 +292,7 @@ class SearchApiIndex extends Entity { /** * Clears this search index and schedules all of its items for re-indexing. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ public function clear() { @@ -318,20 +300,7 @@ class SearchApiIndex extends Entity { return TRUE; } - $server = $this->server(); - if ($server->enabled) { - $server->deleteItems('all', $this); - } - else { - $tasks = variable_get('search_api_tasks', array()); - // If the index was cleared or newly added since the server was last enabled, we don't need to do anything. - if (!isset($tasks[$server->machine_name][$this->machine_name]) - || (array_search('add', $tasks[$server->machine_name][$this->machine_name]) === FALSE - && array_search('clear', $tasks[$server->machine_name][$this->machine_name]) === FALSE)) { - $tasks[$server->machine_name][$this->machine_name][] = 'clear'; - variable_set('search_api_tasks', $tasks); - } - } + $this->server()->deleteItems('all', $this); _search_api_index_reindex($this); module_invoke_all('search_api_index_reindex', $this, TRUE); diff --git a/includes/server_entity.inc b/includes/server_entity.inc index 0436171f..d610262f 100644 --- a/includes/server_entity.inc +++ b/includes/server_entity.inc @@ -82,7 +82,7 @@ class SearchApiServer extends Entity { * @param array $fields * The new field values. * - * @return + * @return int|false * SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had * the specified values. */ @@ -136,6 +136,8 @@ class SearchApiServer extends Entity { } /** + * Reacts to calls of undefined methods on this object. + * * If the service class defines additional methods, not specified in the * SearchApiServiceInterface interface, then they are called via this magic * method. @@ -148,78 +150,220 @@ class SearchApiServer extends Entity { // Proxy methods // For increased clarity, and since some parameters are passed by reference, - // we don't use the __call() magic method for those. + // we don't use the __call() magic method for those. This also gives us the + // opportunity to do additional error handling. + /** + * Form constructor for the server configuration form. + * + * @see SearchApiServiceInterface::configurationForm() + */ public function configurationForm(array $form, array &$form_state) { $this->ensureProxy(); return $this->proxy->configurationForm($form, $form_state); } + /** + * Validation callback for the form returned by configurationForm(). + * + * @see SearchApiServiceInterface::configurationFormValidate() + */ public function configurationFormValidate(array $form, array &$values, array &$form_state) { $this->ensureProxy(); return $this->proxy->configurationFormValidate($form, $values, $form_state); } + /** + * Submit callback for the form returned by configurationForm(). + * + * @see SearchApiServiceInterface::configurationFormSubmit() + */ public function configurationFormSubmit(array $form, array &$values, array &$form_state) { $this->ensureProxy(); return $this->proxy->configurationFormSubmit($form, $values, $form_state); } + /** + * Determines whether this service class supports a given feature. + * + * @see SearchApiServiceInterface::supportsFeature() + */ public function supportsFeature($feature) { $this->ensureProxy(); return $this->proxy->supportsFeature($feature); } + /** + * Displays this server's settings. + * + * @see SearchApiServiceInterface::viewSettings() + */ public function viewSettings() { $this->ensureProxy(); return $this->proxy->viewSettings(); } + /** + * Reacts to the server's creation. + * + * @see SearchApiServiceInterface::postCreate() + */ public function postCreate() { $this->ensureProxy(); return $this->proxy->postCreate(); } + /** + * Notifies this server that its fields are about to be updated. + * + * @see SearchApiServiceInterface::postUpdate() + */ public function postUpdate() { $this->ensureProxy(); return $this->proxy->postUpdate(); } + /** + * Notifies this server that it is about to be deleted from the database. + * + * @see SearchApiServiceInterface::preDelete() + */ public function preDelete() { $this->ensureProxy(); return $this->proxy->preDelete(); } + /** + * Adds a new index to this server. + * + * If an exception in the service class implementation of this method occcurs, + * it will be caught and the operation saved as an pending server task. + * + * @see SearchApiServiceInterface::addIndex() + * @see search_api_server_tasks_add() + */ public function addIndex(SearchApiIndex $index) { $this->ensureProxy(); - return $this->proxy->addIndex($index); + try { + $this->proxy->addIndex($index); + } + catch (SearchApiException $e) { + $vars = array( + '%server' => $this->name, + '%index' => $index->name, + ); + watchdog_exception('search_api', $e, '%type while adding index %index to server %server: !message in %function (line %line of %file).', $vars); + search_api_server_tasks_add($this, __FUNCTION__, $index); + } } + /** + * Notifies the server that the field settings for the index have changed. + * + * If the service class implementation of the method returns TRUE, this will + * automatically take care of marking the items on the index for re-indexing. + * + * If an exception in the service class implementation of this method occcurs, + * it will be caught and the operation saved as an pending server task. + * + * @see SearchApiServiceInterface::fieldsUpdated() + * @see search_api_server_tasks_add() + */ public function fieldsUpdated(SearchApiIndex $index) { $this->ensureProxy(); - return $this->proxy->fieldsUpdated($index); + try { + if ($this->proxy->fieldsUpdated($index)) { + _search_api_index_reindex($index); + return TRUE; + } + } + catch (SearchApiException $e) { + $vars = array( + '%server' => $this->name, + '%index' => $index->name, + ); + watchdog_exception('search_api', $e, '%type while updating the fields of index %index on server %server: !message in %function (line %line of %file).', $vars); + search_api_server_tasks_add($this, __FUNCTION__, $index, isset($index->original) ? $index->original : NULL); + } + return FALSE; } + /** + * Removes an index from this server. + * + * If an exception in the service class implementation of this method occcurs, + * it will be caught and the operation saved as an pending server task. + * + * @see SearchApiServiceInterface::removeIndex() + * @see search_api_server_tasks_add() + */ public function removeIndex($index) { + // When removing an index from a server, it doesn't make any sense anymore to + // delete items from it, or react to other changes. + search_api_server_tasks_delete(NULL, $this, $index); + $this->ensureProxy(); - return $this->proxy->removeIndex($index); + try { + $this->proxy->removeIndex($index); + } + catch (SearchApiException $e) { + $vars = array( + '%server' => $this->name, + '%index' => is_object($index) ? $index->name : $index, + ); + watchdog_exception('search_api', $e, '%type while removing index %index from server %server: !message in %function (line %line of %file).', $vars); + search_api_server_tasks_add($this, __FUNCTION__, $index); + } } + /** + * Indexes the specified items. + * + * @see SearchApiServiceInterface::indexItems() + */ public function indexItems(SearchApiIndex $index, array $items) { $this->ensureProxy(); return $this->proxy->indexItems($index, $items); } + /** + * Deletes indexed items from this server. + * + * If an exception in the service class implementation of this method occcurs, + * it will be caught and the operation saved as an pending server task. + * + * @see SearchApiServiceInterface::deleteItems() + * @see search_api_server_tasks_add() + */ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) { $this->ensureProxy(); - return $this->proxy->deleteItems($ids, $index); + try { + $this->proxy->deleteItems($ids, $index); + } + catch (SearchApiException $e) { + $vars = array( + '%server' => $this->name, + ); + watchdog_exception('search_api', $e, '%type while deleting items from server %server: !message in %function (line %line of %file).', $vars); + search_api_server_tasks_add($this, __FUNCTION__, $index, $ids); + } } + /** + * Creates a query object for searching on an index lying on this server. + * + * @see SearchApiServiceInterface::query() + */ public function query(SearchApiIndex $index, $options = array()) { $this->ensureProxy(); return $this->proxy->query($index, $options); } + /** + * Executes a search on the server represented by this object. + * + * @see SearchApiServiceInterface::search() + */ public function search(SearchApiQueryInterface $query) { $this->ensureProxy(); return $this->proxy->search($query); diff --git a/includes/service.inc b/includes/service.inc index c6edc957..6442f9b8 100644 --- a/includes/service.inc +++ b/includes/service.inc @@ -1,10 +1,20 @@ - * listing all relevant settings is preferred. + * Displays this server's settings. + * + * Output can be HTML or a render array, a
    listing all relevant settings + * is preferred. */ public function viewSettings(); /** + * Reacts to the server's creation. + * * Called once, when the server is first created. Allows it to set up its * necessary infrastructure. */ public function postCreate(); /** - * Notifies this server that its fields are about to be updated. The server's - * $original property can be used to inspect the old property values. + * Notifies this server that its fields are about to be updated. + * + * The server's $original property can be used to inspect the old property + * values. * - * @return + * @return bool * TRUE, if the update requires reindexing of all content on the server. */ public function postUpdate(); /** - * Notifies this server that it is about to be deleted from the database and - * should therefore clean up, if appropriate. + * Notifies this server that it is about to be deleted from the database. + * + * This should execute any necessary cleanup operations. * * Note that you shouldn't call the server's save() method, or any * methods that might do that, from inside of this method as the server isn't @@ -112,18 +136,21 @@ interface SearchApiServiceInterface { public function preDelete(); /** - * Add a new index to this server. + * Adds a new index to this server. * * If the index was already added to the server, the object should treat this * as if removeIndex() and then addIndex() were called. * * @param SearchApiIndex $index * The index to add. + * + * @throws SearchApiException + * If an error occurred while adding the index. */ public function addIndex(SearchApiIndex $index); /** - * Notify the server that the field settings for the index have changed. + * Notifies the server that the field settings for the index have changed. * * If any user action is necessary as a result of this, the method should * use drupal_set_message() to notify the user. @@ -134,11 +161,14 @@ interface SearchApiServiceInterface { * @return bool * TRUE, if this change affected the server in any way that forces it to * re-index the content. FALSE otherwise. + * + * @throws SearchApiException + * If an error occurred while reacting to the change of fields. */ public function fieldsUpdated(SearchApiIndex $index); /** - * Remove an index from this server. + * Removes an index from this server. * * This might mean that the index has been deleted, or reassigned to a * different server. If you need to distinguish between these cases, inspect @@ -152,11 +182,14 @@ interface SearchApiServiceInterface { * @param $index * Either an object representing the index to remove, or its machine name * (if the index was completely deleted). + * + * @throws SearchApiException + * If an error occurred while removing the index. */ public function removeIndex($index); /** - * Index the specified items. + * Indexes the specified items. * * @param SearchApiIndex $index * The search index for which items should be indexed. @@ -187,7 +220,7 @@ interface SearchApiServiceInterface { public function indexItems(SearchApiIndex $index, array $items); /** - * Delete items from an index on this server. + * Deletes indexed items from this server. * * Might be either used to delete some items (given by their ids) from a * specified index, or all items from that index, or all items from all @@ -200,11 +233,14 @@ interface SearchApiServiceInterface { * @param SearchApiIndex $index * The index from which items should be deleted, or NULL if all indexes on * this server should be cleared (then, $ids has to be 'all'). + * + * @throws SearchApiException + * If an error occurred while trying to delete the items. */ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL); /** - * Create a query object for searching on an index lying on this server. + * Creates a query object for searching on an index lying on this server. * * @param SearchApiIndex $index * The index to search on. diff --git a/search_api.admin.inc b/search_api.admin.inc index 8161ff3c..8bec3fb5 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -611,17 +611,10 @@ function search_api_admin_add_index(array $form, array &$form_state) { '#default_value' => '', '#options' => array('' => t('< No server >')) ); - $servers = search_api_server_load_multiple(FALSE); + $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); // List enabled servers first. foreach ($servers as $server) { - if ($server->enabled) { - $form['server']['#options'][$server->machine_name] = $server->name; - } - } - foreach ($servers as $server) { - if (!$server->enabled) { - $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); - } + $form['server']['#options'][$server->machine_name] = $server->name; } $form['read_only'] = array( '#type' => 'checkbox', @@ -678,8 +671,8 @@ function search_api_admin_add_index_submit(array $form, array &$form_state) { $values = $form_state['values']; - // Validation of whether the server of an enabled index is also enabled is - // done in the *_insert() function. + // Validation of whether the server of an index is enabled is done in the + // SearchApiIndex::save() method. search_api_index_insert($values); drupal_set_message(t('The index was successfully created. Please set up its indexed fields now.')); @@ -966,17 +959,10 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#default_value' => $index->server, '#options' => array('' => t('< No server >')) ); - $servers = search_api_server_load_multiple(FALSE); + $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); // List enabled servers first. foreach ($servers as $server) { - if ($server->enabled) { - $form['server']['#options'][$server->machine_name] = $server->name; - } - } - foreach ($servers as $server) { - if (!$server->enabled) { - $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); - } + $form['server']['#options'][$server->machine_name] = $server->name; } $form['read_only'] = array( '#type' => 'checkbox', @@ -1819,11 +1805,20 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields'; } - /** * Helper function for displaying a generic confirmation form. * - * @return + * @param $type + * The type of entity (not the real "entity type"). Either "server" or + * "index". + * @param $action + * The action that would be executed for this entity after confirming. Either + * "disable" or "delete". + * @param Entity $entity + * The entity for which the action would be performed. Must have a "name" + * property. + * + * @return array|false * Either a form array, or FALSE if this combination of type and action is * not supported. */ @@ -1835,8 +1830,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio $text = array( t('Disable server @name', array('@name' => $entity->name)), t('Do you really want to disable this server?'), - t('This will disable both the server and all associated indexes. ' . - "Searches on these indexes won't be available until they are re-enabled."), + t('This will disconnect all indexes from this server and disable them. Searches on these indexes will not be available until they are added to another server and re-enabled. All indexed data (except for read-only indexes) on this server will be cleared.'), t('The server and its indexes were successfully disabled.'), ); break; diff --git a/search_api.install b/search_api.install index f39608c8..9a113750 100644 --- a/search_api.install +++ b/search_api.install @@ -191,6 +191,47 @@ function search_api_schema() { 'primary key' => array('item_id', 'index_id'), ); + $schema['search_api_task'] = array( + 'description' => 'Stores pending tasks for servers.', + 'fields' => array( + 'id' => array( + 'description' => 'An integer identifying this task.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'server_id' => array( + 'description' => 'The {search_api_server}.machine_name for which this task should be executed.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'type' => array( + 'description' => 'A keyword identifying the type of task that should be executed.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'index_id' => array( + 'description' => 'The {search_api_index}.machine_name to which this task pertains, if applicable for this type.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => FALSE, + ), + 'data' => array( + 'description' => 'Some data needed for the task, might be optional depending on the type.', + 'type' => 'text', + 'size' => 'medium', + 'serialize' => TRUE, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'server' => array('server_id'), + ), + 'primary key' => array('id'), + ); + return $schema; } @@ -336,7 +377,6 @@ function search_api_disable() { * Implements hook_uninstall(). */ function search_api_uninstall() { - variable_del('search_api_tasks'); variable_del('search_api_index_worker_callback_runtime'); } @@ -826,3 +866,103 @@ function search_api_update_7115() { ->condition('changed', 0, '<') ->execute(); } + +/** + * Transfers the tasks for disabled servers to a separate database table. + */ +function search_api_update_7116() { + // Create table. + $table = array( + 'description' => 'Stores pending tasks for servers.', + 'fields' => array( + 'id' => array( + 'description' => 'An integer identifying this task.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'server_id' => array( + 'description' => 'The {search_api_server}.machine_name for which this task should be executed.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'type' => array( + 'description' => 'A keyword identifying the type of task that should be executed.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'index_id' => array( + 'description' => 'The {search_api_index}.machine_name to which this task pertains, if applicable for this type.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => FALSE, + ), + 'data' => array( + 'description' => 'Some data needed for the task, might be optional depending on the type.', + 'type' => 'text', + 'size' => 'medium', + 'serialize' => TRUE, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'server' => array('server_id'), + ), + 'primary key' => array('id'), + ); + db_create_table('search_api_task', $table); + + // Collect old tasks. + $tasks = array(); + foreach (variable_get('search_api_tasks', array()) as $server => $indexes) { + foreach ($indexes as $index => $old_tasks) { + if (in_array('clear all', $old_tasks)) { + $tasks[] = array( + 'server_id' => $server, + 'type' => 'deleteItems', + ); + } + if (in_array('remove', $old_tasks)) { + $tasks[] = array( + 'server_id' => $server, + 'type' => 'removeIndex', + 'index_id' => $index, + ); + } + } + } + variable_del('search_api_tasks'); + + $select = db_select('search_api_index', 'i') + ->fields('i', array('machine_name', 'server')); + $select->innerJoin('search_api_server', 's', 'i.server = s.machine_name AND s.enabled = 0'); + $index_ids = array(); + foreach ($select->execute() as $index) { + $index_ids[] = $index->machine_name; + $tasks[] = array( + 'server_id' => $$index->server, + 'type' => 'removeIndex', + 'index_id' => $index->machine_name, + ); + } + if ($index_ids) { + db_update('search_api_index') + ->fields(array( + 'enabled' => 0, + 'server' => NULL, + )) + ->condition('machine_name', $index_ids) + ->execute(); + } + + if ($tasks) { + $insert = db_insert('search_api_task') + ->fields(array('server_id', 'type', 'index_id', 'data')); + foreach ($tasks as $task) { + $insert->values($task); + } + $insert->execute(); + } +} diff --git a/search_api.module b/search_api.module index 51d67391..c52eca4f 100644 --- a/search_api.module +++ b/search_api.module @@ -1,5 +1,10 @@ TRUE, @@ -319,7 +333,8 @@ function search_api_cron() { // Exceptions will probably be caused by the server in most cases. // Therefore, don't index for any index on this server. $ignored_servers[$index->server] = TRUE; - watchdog_exception('search_api', $e); + $vars['%index'] = $index->name; + watchdog_exception('search_api', $e, '%type while trying to index items on %index: !message in %function (line %line of %file).', $vars); } } if (!$num) { @@ -540,50 +555,11 @@ function search_api_search_api_server_update(SearchApiServer $server) { } if (!empty($server->original) && $server->enabled != $server->original->enabled) { if ($server->enabled) { - // Were there any changes in the server's indexes while it was disabled? - $tasks = variable_get('search_api_tasks', array()); - if (isset($tasks[$server->machine_name])) { - foreach ($tasks[$server->machine_name] as $index_id => $index_tasks) { - $index = search_api_index_load($index_id); - foreach ($index_tasks as $task) { - switch ($task) { - case 'add': - $server->addIndex($index); - break; - case 'clear': - $server->deleteItems('all', $index); - break; - case 'clear all': - // Would normally be used with a fake index ID of "", since it - // doesn't matter. - $server->deleteItems('all'); - break; - case 'fields': - if ($server->fieldsUpdated($index)) { - _search_api_index_reindex($index); - } - break; - case 'remove': - $server->removeIndex($index ? $index : $index_id); - break; - default: - if (substr($task, 0, 7) == 'delete-') { - $id = substr($task, 7); - $server->deleteItems(array($id), $index); - } - else { - watchdog('search_api', t('Unknown task "@task" for server "@name".', array('@task' => $task, '@name' => $server->machine_name)), NULL, WATCHDOG_WARNING); - } - } - } - } - unset($tasks[$server->machine_name]); - variable_set('search_api_tasks', $tasks); - } + search_api_server_tasks_check($server); } else { - foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name, 'enabled' => 1)) as $index) { - $index->update(array('enabled' => 0)); + foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) { + $index->update(array('enabled' => 0, 'server' => NULL)); } } } @@ -607,9 +583,7 @@ function search_api_search_api_server_delete(SearchApiServer $server) { $index->update(array('server' => NULL, 'enabled' => FALSE)); } - $tasks = variable_get('search_api_tasks', array()); - unset($tasks[$server->machine_name]); - variable_set('search_api_tasks', $tasks); + search_api_server_tasks_delete(NULL, $server); } /** @@ -647,31 +621,14 @@ function search_api_search_api_index_update(SearchApiIndex $index) { $old_server = search_api_server_load($index->original->server); // The server might have changed because the old one was deleted: if ($old_server) { - if ($old_server->enabled) { - $old_server->removeIndex($index); - } - else { - $tasks = variable_get('search_api_tasks', array()); - // When we add or remove an index, we can ignore all other tasks. - $tasks[$old_server->machine_name][$index->machine_name] = array('remove'); - variable_set('search_api_tasks', $tasks); - } + $old_server->removeIndex($index); } } if ($index->server) { $new_server = $index->server(TRUE); // If the server is enabled, we call addIndex(); otherwise, we save the task. - if ($new_server->enabled) { - $new_server->addIndex($index); - } - else { - $tasks = variable_get('search_api_tasks', array()); - // When we add or remove an index, we can ignore all other tasks. - $tasks[$new_server->machine_name][$index->machine_name] = array('add'); - variable_set('search_api_tasks', $tasks); - unset($new_server); - } + $new_server->addIndex($index); } // We also have to re-index all content. @@ -686,8 +643,8 @@ function search_api_search_api_index_update(SearchApiIndex $index) { $new_fields = $new_fields['fields']; if ($old_fields != $new_fields) { cache_clear_all($index->getCacheId(), 'cache', TRUE); - if ($index->server && $index->server()->fieldsUpdated($index)) { - _search_api_index_reindex($index); + if ($index->server) { + $index->server()->fieldsUpdated($index); } } @@ -1211,18 +1168,156 @@ function search_api_track_item_delete($type, array $item_ids) { foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { if ($index->server) { $server = $index->server(); - if ($server->enabled) { - $server->deleteItems($item_ids, $index); + $server->deleteItems($item_ids, $index); + } + } +} + +/** + * Checks for pending tasks on one or all enabled search servers. + * + * @param SearchApiServer|null $server + * (optional) The server whose tasks should be checked. If not given, the + * tasks for all enabled servers are checked. + * + * @return bool + * TRUE if all tasks (for the specific server, if $server was given) were + * executed successfully, or if there were no tasks. FALSE if there are still + * pending tasks. + */ +function search_api_server_tasks_check(SearchApiServer $server = NULL) { + $select = db_select('search_api_task', 't') + ->fields('t') + // Only retrieve tasks we can handle. + ->condition('t.type', array('addIndex', 'fieldsUpdated', 'removeIndex', 'deleteItems')); + if ($server) { + $select->condition('t.server_id', $server->machine_name); + } + else { + $select->innerJoin('search_api_server', 's', 't.server_id = s.machine_name AND s.enabled = 1'); + // By ordering by the server, we can later just load them when we reach them + // while looping through the tasks. It is very unlikely there will be tasks + // for more than one or two servers, so a *_load_multiple() probably + // wouldn't bring any significant advantages, but complicate the code. + $select->orderBy('t.server_id'); + } + // Store a count query for later checking whether all tasks were processed + // successfully. + $count_query = $select->countQuery(); + + // Sometimes the order of tasks might be important, so make sure to order by + // the task ID (which should be in order of insertion). + $select->orderBy('t.id'); + $tasks = $select->execute(); + + $executed_tasks = array(); + foreach ($tasks as $task) { + if (!$server || $server->machine_name != $task->server_id) { + $server = search_api_server_load($task->server_id); + if (!$server) { + continue; } - else { - $tasks = variable_get('search_api_tasks', array()); - foreach ($item_ids as $id) { - $tasks[$server->machine_name][$index->machine_name][] = 'delete-' . $id; + } + switch ($task->type) { + case 'addIndex': + $index = search_api_index_load($task->index_id); + if ($index) { + $server->addIndex($index); } - variable_set('search_api_tasks', $tasks); - } + break; + + case 'fieldsUpdated': + $index = search_api_index_load($task->index_id); + if ($index) { + if ($task->data) { + $index->original = unserialize($task->data); + } + $server->fieldsUpdated($index); + } + break; + + case 'removeIndex': + $index = search_api_index_load($task->index_id); + if ($index) { + $server->removeIndex($index ? $index : $task->index_id); + } + break; + + case 'deleteItems': + $ids = $task->data ? unserialize($task->data) : 'all'; + $index = $task->index_id ? search_api_index_load($task->index_id) : NULL; + // Since a failed load returns (for stupid menu handler reasons) FALSE, + // not NULL, we have to make doubly sure here not to pass an invalid + // value (and cause a fatal error). + $index = $index ? $index : NULL; + $server->deleteItems($ids, $index); + break; + + default: + // This should never happen. + continue; } + $executed_tasks[] = $task->id; } + + // If there were no tasks (we recognized), return TRUE. + if (!$executed_tasks) { + return TRUE; + } + // Otherwise, delete the executed tasks and check if new tasks were created. + search_api_server_tasks_delete($executed_tasks); + return $count_query->execute()->fetchField() === 0; +} + +/** + * Adds an entry into a server's list of pending tasks. + * + * @param SearchApiServer $server + * The server for which a task should be remembered. + * @param $type + * The type of task to perform. + * @param SearchApiIndex|string|null $index + * (optional) If applicable, the index to which the task pertains (or its + * machine name). + * @param mixed $data + * (optional) If applicable, some further data necessary for the task. + */ +function search_api_server_tasks_add(SearchApiServer $server, $type, $index = NULL, $data = NULL) { + db_insert('search_api_task') + ->fields(array( + 'server_id' => $server->machine_name, + 'type' => $type, + 'index_id' => $index ? (is_object($index) ? $index->machine_name : $index) : NULL, + 'data' => isset($data) ? serialize($data) : NULL, + )) + ->execute(); +} + +/** + * Removes pending server tasks from the list. + * + * @param array|null $ids + * (optional) The IDs of the pending server tasks to delete. Set to NULL + * to not filter by IDs. + * @param SearchApiServer|null $server + * (optional) A server for which the tasks should be deleted. Set to NULL to + * delete tasks from all servers. + * @param SearchApiIndex|string|null $index + * (optional) An index (or its machine name) for which the tasks should be + * deleted. Set to NULL to delete tasks for all indexes. + */ +function search_api_server_tasks_delete(array $ids = NULL, SearchApiServer $server = NULL, $index = NULL) { + $delete = db_delete('search_api_task'); + if ($ids) { + $delete->condition('id', $ids); + } + if ($server) { + $delete->condition('server_id', $server->machine_name); + } + if ($index) { + $delete->condition('index_id', $index->machine_name); + } + $delete->execute(); } /** @@ -1347,6 +1442,13 @@ function search_api_index_items(SearchApiIndex $index, $limit = -1) { * If any error occurs during indexing. */ function search_api_index_specific_items(SearchApiIndex $index, array $ids) { + // Before doing anything else, check whether there are pending tasks that need + // to be executed on the server. It might be important that they are executed + // before any indexing occurs. + if (!search_api_server_tasks_check($index->server())) { + throw new SearchApiException(t('Could not index items since important pending server tasks could not be performed.')); + } + $items = $index->loadItems($ids); // Clone items because data alterations may alter them. $cloned_items = array(); @@ -2246,13 +2348,13 @@ function search_api_server_insert(array $values) { /** * Changes a server's settings. * - * @param $id + * @param string|int $id * The ID or machine name of the server whose values should be changed. * @param array $fields * The new field values to set. The enabled field can't be set this way, use * search_api_server_enable() and search_api_server_disable() instead. * - * @return + * @return int|false * 1 if fields were changed, 0 if the fields already had the desired values. * FALSE on failure. */ @@ -2263,13 +2365,14 @@ function search_api_server_edit($id, array $fields) { } /** - * Enables a search server. Will also check for remembered tasks for this server - * and execute them. + * Enables a search server. * - * @param $id + * Will also check for remembered tasks for this server and execute them. + * + * @param string|int $id * The ID or machine name of the server to enable. * - * @return + * @return int|false * 1 on success, 0 or FALSE on failure. */ function search_api_server_enable($id) { @@ -2279,12 +2382,14 @@ function search_api_server_enable($id) { } /** - * Disables a search server, along with all associated indexes. + * Disables a search server. * - * @param $id + * Will also disable all associated indexes and remove them from the server. + * + * @param string|int $id * The ID or machine name of the server to disable. * - * @return + * @return int|false * 1 on success, 0 or FALSE on failure. */ function search_api_server_disable($id) { @@ -2316,12 +2421,12 @@ function search_api_server_delete($id) { * @param $reset * Whether to reset the internal cache. * - * @return SearchApiIndex - * A completely loaded index object, or NULL if no such index exists. + * @return SearchApiIndex|false + * A completely loaded index object, or FALSE if no such index exists. */ function search_api_index_load($id, $reset = FALSE) { $ret = search_api_index_load_multiple(array($id), array(), $reset); - return $ret ? reset($ret) : FALSE; + return reset($ret); } /** @@ -2427,12 +2532,12 @@ function search_api_index_insert(array $values) { /** * Changes an index' settings. * - * @param $id - * The edited index' id. + * @param int|string $id + * The edited index' ID or machine name. * @param array $fields * The new field values to set. * - * @return + * @return int|false * 1 if fields were changed, 0 if the fields already had the desired values. * FALSE on failure. */ @@ -2445,12 +2550,12 @@ function search_api_index_edit($id, array $fields) { /** * Changes an index' indexed field settings. * - * @param $id + * @param int|string $id * The ID or machine name of the index whose fields should be changed. * @param array $fields * The new indexed field settings. * - * @return + * @return int|false * 1 if the field settings were changed, 0 if they already had the desired * values. FALSE on failure. */ @@ -2696,8 +2801,18 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, $to_index = min($context['sandbox']['limit'] - $context['sandbox']['progress'], $context['sandbox']['batch_size']); // Index the items. - $indexed = search_api_index_items($index, $to_index); - $context['results']['indexed'] += $indexed; + try { + $indexed = search_api_index_items($index, $to_index); + $context['results']['indexed'] += $indexed; + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + $vars['@message'] = $e->getMessage(); + $context['message'] = t('An error occurred during indexing: @message.', $vars); + $context['finished'] = 1; + $context['results']['not indexed'] += $context['sandbox']['limit'] - $context['sandbox']['progress']; + return; + } // Display progress message. if ($indexed > 0) { diff --git a/search_api.test b/search_api.test index 9e098aab..b7e0752e 100644 --- a/search_api.test +++ b/search_api.test @@ -104,6 +104,26 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->disableModules(); } + /** + * Returns the test server in use by this test case. + * + * @return SearchApiServer + * The test server. + */ + protected function server() { + return search_api_server_load($this->server_id, TRUE); + } + + /** + * Returns the test index in use by this test case. + * + * @return SearchApiIndex + * The test index. + */ + protected function index() { + return search_api_index_load($this->index_id, TRUE); + } + /** * Inserts some test items into the database, via the test module. * @@ -170,7 +190,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), 'The index was successfully created.'); $found = strpos($this->getUrl(), 'admin/config/search/search_api/index/' . $id) !== FALSE; $this->assertTrue($found, 'Correct redirect.'); - $index = search_api_index_load($id, TRUE); + $index = $this->index(); $this->assertEqual($index->name, $values['name'], 'Name correctly inserted.'); $this->assertEqual($index->item_type, $values['item_type'], 'Index item type correctly inserted.'); $this->assertFalse($index->enabled, 'Status correctly inserted.'); @@ -285,7 +305,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('The server was successfully created.')); $found = strpos($this->getUrl(), 'admin/config/search/search_api/server/' . $id) !== FALSE; $this->assertTrue($found, 'Correct redirect.'); - $server = search_api_server_load($id, TRUE); + $server = $this->server(); $this->assertEqual($server->name, $values['name'], 'Name correctly inserted.'); $this->assertTrue($server->enabled, 'Status correctly inserted.'); $this->assertEqual($server->description, $values['description'], 'Description correctly inserted.'); @@ -352,8 +372,8 @@ class SearchApiWebTest extends DrupalWebTestCase { // different page requests), the static cache in this page request // (executing the tests) will get stale. Therefore, we clear it before // executing the search. - search_api_index_load($this->index_id, TRUE); - search_api_server_load($this->server_id, TRUE); + $this->index(); + $this->server(); $query = search_api_query($this->index_id); if ($offset || $limit) { @@ -414,7 +434,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->checkIndexStatus(10); // Reset the static cache for the server. - search_api_server_load($this->server_id, TRUE); + $this->server(); } /** @@ -514,7 +534,7 @@ class SearchApiWebTest extends DrupalWebTestCase { // count as indexed for the Search API, but won't be returned in searches. // We do this so we have finer-grained control over the order in which items // are indexed. - search_api_server_load($this->server_id, TRUE)->deleteItems(); + $this->server()->deleteItems(); $results = $this->doSearch(); $this->assertEqual($results['result count'], 0, 'Indexed items were successfully deleted from the server.'); $this->assertEqual(array_keys($results['results']), array(), 'Indexed items were successfully deleted from the server.'); @@ -576,6 +596,86 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertEqual(array_keys($results['results']), array(2, 5, 8, 11, 12, 13, 14), 'Indexing order test 4: correct results.'); } + /** + * Tests whether the server tasks system works correctly. + * + * Uses the "search_api_test_error_state" variable to trigger exceptions in + * the test service class and asserts that the Search API reacts correctly and + * re-attempts the operation on the next cron run. + */ + protected function checkServerTasks() { + // Make sure none of the previous operations added any tasks. + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 0, 'No server tasks were previously saved.'); + + // Set error state for test service, so all operations will fail. + variable_set('search_api_test_error_state', TRUE); + + // Delete some items. + $this->drupalGet('search_api_test/delete/8'); + $this->drupalGet('search_api_test/delete/12'); + + // Assert that the indexed items haven't changed yet. + $results = $this->doSearch(); + $this->assertEqual(array_keys($results['results']), array(2, 5, 8, 11, 12, 13, 14), 'During error state, no indexed items were deleted.'); + + // Check that tasks were correctly inserted. + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 2, 'Server tasks for deleted items were saved.'); + + // Now reset the error state variable and run cron to delete the items. + variable_set('search_api_test_error_state', FALSE); + $this->cronRun(); + + // Assert that the indexed items were indeed deleted from the server. + $results = $this->doSearch(); + $this->assertEqual(array_keys($results['results']), array(2, 5, 11, 13, 14), 'Pending "delete item" server tasks were correctly executed during the cron run.'); + + // Check that the tasks were correctly deleted. + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 0, 'Server tasks were correctly deleted after being executed.'); + + // Now we first delete more items, then disable the server (thereby removing + // the index from it) – all while in error state. + variable_set('search_api_test_error_state', TRUE); + $this->drupalGet('search_api_test/delete/14'); + $this->drupalGet('search_api_test/delete/2'); + $settings['enabled'] = 0; + $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $settings, t('Save settings')); + + // Check whether the index was correctly removed from the server. + $this->assertEqual($this->index()->server(), NULL, 'The index was successfully set to have no server.'); + $exception = FALSE; + try { + $this->doSearch(); + } + catch (SearchApiException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, 'Searching on the index failed with an exception.'); + + // Check that only one task – to remove the index from the server – is now + // present in the tasks table. + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 1, 'Only the "remove index" task is present in the server tasks.'); + + // Reset the error state variable, re-enable the server. + variable_set('search_api_test_error_state', FALSE); + $settings['enabled'] = 1; + $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $settings, t('Save settings')); + + // Check whether the index was really removed from the server now. + $server = $this->server(); + $this->assertTrue(empty($server->options['indexes'][$this->index_id]), 'The index was removed from the server after cron ran.'); + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 0, 'Server tasks were correctly deleted after being executed.'); + + // Put the index back on the server and index some items for the next tests. + $settings = array('server' => $this->server_id); + $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $settings, t('Save settings')); + $this->cronRun(); + } + /** * Tests whether editing the server works correctly. */ @@ -605,13 +705,26 @@ class SearchApiWebTest extends DrupalWebTestCase { * Tests whether deleting the server works correctly. * * The index still lying on the server should be disabled and removed from it. + * Also, any tasks with that server's ID should be deleted. */ protected function deleteServer() { + // Insert some dummy tasks to check for. + $server = $this->server(); + search_api_server_tasks_add($server, 'foo'); + search_api_server_tasks_add($server, 'bar', $this->index()); + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 2, 'Dummy tasks were added.'); + + // Delete the server. $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm')); $this->assertNoText('test-name-foo', 'Server no longer listed.'); $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}"); $this->assertNoText(t('Server'), 'The index was removed from the server.'); $this->assertText(t('disabled'), 'The index was disabled.'); + + // Check whether the tasks were correctly deleted. + $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField(); + $this->assertEqual($task_count, 0, 'Remaining server tasks were correctly deleted.'); } /** @@ -635,7 +748,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertFalse(db_table_exists('search_api_server'), 'Search server table was successfully removed.'); $this->assertFalse(db_table_exists('search_api_index'), 'Search index table was successfully removed.'); $this->assertFalse(db_table_exists('search_api_item'), 'Index items table was successfully removed.'); - $this->assertNull(variable_get('search_api_tasks'), 'Tasks variable was correctly removed.'); + $this->assertFalse(db_table_exists('search_api_task'), 'Server tasks table was successfully removed.'); $this->assertNull(variable_get('search_api_index_worker_callback_runtime'), 'Worker runtime variable was correctly removed.'); } diff --git a/tests/search_api_test.module b/tests/search_api_test.module index 0fd527f5..00a2028d 100644 --- a/tests/search_api_test.module +++ b/tests/search_api_test.module @@ -23,6 +23,12 @@ function search_api_test_menu() { 'page arguments' => array(2), 'access callback' => TRUE, ), + 'search_api_test/delete/%search_api_test' => array( + 'title' => 'Delete items', + 'page callback' => 'search_api_test_delete', + 'page arguments' => array(2), + 'access callback' => TRUE, + ), ); } @@ -87,6 +93,14 @@ function search_api_test_touch($entity) { module_invoke_all('entity_update', $entity, 'search_api_test'); } +/** + * Menu callback for marking a "search_api_test" entity as changed. + */ +function search_api_test_delete($entity) { + db_delete('search_api_test')->condition('id', $entity->id)->execute(); + module_invoke_all('entity_delete', $entity, 'search_api_test'); +} + /** * Implements hook_entity_info(). */ @@ -226,6 +240,29 @@ class SearchApiTestService extends SearchApiAbstractService { return $form; } + /** + * {@inheritdoc} + */ + public function addIndex(SearchApiIndex $index) { + $this->checkErrorState(); + } + + /** + * {@inheritdoc} + */ + public function fieldsUpdated(SearchApiIndex $index) { + $this->checkErrorState(); + return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0; + } + + /** + * {@inheritdoc} + */ + public function removeIndex($index) { + $this->checkErrorState(); + parent::removeIndex($index); + } + /** * Implements SearchApiServiceInterface::indexItems(). * @@ -235,6 +272,7 @@ class SearchApiTestService extends SearchApiAbstractService { * that ID will not be indexed. */ public function indexItems(SearchApiIndex $index, array $items) { + $this->checkErrorState(); // Refuse to index the item with the same ID as the // "search_api_test_indexing_break" variable, if it is set. $exclude = variable_get('search_api_test_indexing_break', 8); @@ -267,6 +305,7 @@ class SearchApiTestService extends SearchApiAbstractService { * {@inheritdoc} */ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) { + $this->checkErrorState(); if ($ids == 'all') { if ($index) { $this->options['indexes'][$index->machine_name] = array(); @@ -321,10 +360,15 @@ class SearchApiTestService extends SearchApiAbstractService { } /** - * {@inheritdoc} + * Throws an exception if the "search_api_test_error_state" variable is set. + * + * @throws SearchApiException + * If the "search_api_test_error_state" variable is set. */ - public function fieldsUpdated(SearchApiIndex $index) { - return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0; + protected function checkErrorState() { + if (variable_get('search_api_test_error_state', FALSE)) { + throw new SearchApiException(); + } } } From 6158ba1289b008ed3e653acee5056976fabb90cc Mon Sep 17 00:00:00 2001 From: frando Date: Fri, 15 Nov 2013 10:37:37 +0100 Subject: [PATCH 053/278] Issue #2128529 by Frando, drunken monkey: Added a way for facet query type plugins to pass options to the search query. --- CHANGELOG.txt | 2 ++ contrib/search_api_facetapi/plugins/facetapi/adapter.inc | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e3ee0d63..bdbecbc3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2128529 by Frando, drunken monkey: Added a way for facet query type plugins + to pass options to the search query. - #1551302 by drunken monkey: Fixed the server tasks system. - #2135363 by drumm, drunken monkey: Added support for Views' use_count_query() method. diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index 23dde9f6..824350ad 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -109,7 +109,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { public function addFacet(array $facet, SearchApiQueryInterface $query) { if (isset($this->fields[$facet['name']])) { $options = &$query->getOptions(); - $options['search_api_facets'][$facet['name']] = $this->fields[$facet['name']]; + $facet_info = $this->fields[$facet['name']]; + if (!empty($facet['query_options'])) { + // Let facet-specific query options override the set options. + $facet_info = $facet['query_options'] + $facet_info; + } + $options['search_api_facets'][$facet['name']] = $facet_info; } } From 3ff385f255611c241140b46668b60fa5508d291c Mon Sep 17 00:00:00 2001 From: stborchert Date: Fri, 15 Nov 2013 10:42:36 +0100 Subject: [PATCH 054/278] Issue #2128947 by stBorchert, drunken monkey: Fixed facet handling for multiple searches on a page. --- CHANGELOG.txt | 2 ++ .../plugins/facetapi/query_type_date.inc | 11 +++++++++++ .../plugins/facetapi/query_type_term.inc | 15 +++++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bdbecbc3..dd13190e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2128947 by stBorchert, drunken monkey: Fixed facet handling for multiple + searches on a page. - #2128529 by Frando, drunken monkey: Added a way for facet query type plugins to pass options to the search query. - #1551302 by drunken monkey: Fixed the server tasks system. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 9a783096..b045fc8f 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -37,6 +37,17 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue public function execute($query) { // Return terms for this facet. $this->adapter->addFacet($this->facet, $query); + + $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings; + + // First check if the facet is enabled for this search. + $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE; + $facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array(); + if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) { + // Facet is not enabled for this search ID. + return; + } + // Change limit to "unlimited" (-1). $options = &$query->getOptions(); if (!empty($options['search_api_facets'][$this->facet['name']])) { diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 33dacc16..587598a3 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -30,11 +30,17 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // Return terms for this facet. $this->adapter->addFacet($this->facet, $query); - $settings = $this->adapter->getFacet($this->facet)->getSettings(); - // Adds the operator parameter. - $operator = $settings->settings['operator']; + $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings; + + // First check if the facet is enabled for this search. + $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE; + $facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array(); + if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) { + // Facet is not enabled for this search ID. + return; + } - // Add active facet filters. + // Retrieve the active facet filters. $active = $this->adapter->getActiveItems($this->facet); if (empty($active)) { return; @@ -42,6 +48,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // Create the facet filter, and add a tag to it so that it can be easily // identified down the line by services when they need to exclude facets. + $operator = $settings['operator']; if ($operator == FACETAPI_OPERATOR_AND) { $conjunction = 'AND'; } From 643d50b3ee817170f17d94e9c9de8689883e64f2 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 15 Nov 2013 10:50:17 +0100 Subject: [PATCH 055/278] Issue #2128001 by drunken monkey: Fixed the logic of the "contains none of these words" fulltext operator. --- CHANGELOG.txt | 2 ++ .../search_api_views/includes/handler_filter_fulltext.inc | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd13190e..ebd46ed9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2128001 by drunken monkey: Fixed the logic of the "contains none of these + words" fulltext operator. - #2128947 by stBorchert, drunken monkey: Fixed facet handling for multiple searches on a page. - #2128529 by Frando, drunken monkey: Added a way for facet query type plugins diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index db8e0c91..f2647fb6 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -158,9 +158,9 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex return; } - // If the operator was set to OR, set it as the conjunction. (AND is set by - // default.) - if ($this->operator === 'OR') { + // If the operator was set to OR or NOT, set OR as the conjunction. (It is + // also set for NOT since otherwise it would be "not all of these words".) + if ($this->operator != 'AND') { $this->query->setOption('conjunction', $this->operator); } From 35c829f98e25de82d0b95e615abf5595b0ee3cad Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 15 Nov 2013 16:15:31 +0100 Subject: [PATCH 056/278] Minimal doc comment fix. --- includes/datasource.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/datasource.inc b/includes/datasource.inc index e325644a..f293fa70 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -394,7 +394,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * * The data structure uses largely the format specified in * hook_entity_property_info(). However, the first level of keys (containing - * the entity types) is omitted, and the "property" key is called + * the entity types) is omitted, and the "properties" key is called * "property info" instead. So, an example return value would look like this: * * @code From a9295a178542d79e41d8345a91cdb888f9cdf5c2 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 19 Nov 2013 08:40:52 +0100 Subject: [PATCH 057/278] Some documentation and other comment fixes. --- .../includes/handler_argument.inc | 5 + .../includes/handler_argument_fulltext.inc | 5 + .../handler_argument_more_like_this.inc | 5 + .../includes/handler_argument_string.inc | 5 + .../includes/handler_filter.inc | 5 + .../includes/handler_filter_boolean.inc | 5 + .../includes/handler_filter_date.inc | 5 + .../includes/handler_filter_fulltext.inc | 5 + .../includes/handler_filter_text.inc | 5 + .../includes/handler_sort.inc | 5 + contrib/search_api_views/includes/query.inc | 5 + .../search_api_views/search_api_views.install | 1 + .../search_api_views/search_api_views.module | 5 + .../search_api_views.views.inc | 5 + includes/callback_add_aggregation.inc | 5 + includes/callback_add_hierarchy.inc | 60 ++------- includes/callback_add_url.inc | 5 + includes/callback_add_viewed_entity.inc | 5 + includes/callback_language_control.inc | 49 ++----- includes/datasource.inc | 10 +- includes/exception.inc | 5 + includes/index_entity.inc | 5 + includes/processor.inc | 5 + includes/processor_html_filter.inc | 5 + includes/processor_ignore_case.inc | 5 + includes/processor_stopwords.inc | 7 +- includes/processor_tokenizer.inc | 5 + includes/processor_transliteration.inc | 5 + includes/query.inc | 5 + includes/server_entity.inc | 5 + search_api.admin.css | 5 + search_api.admin.inc | 121 ++++++++++++------ search_api.admin.js | 6 +- search_api.module | 36 +++--- tests/search_api_test.module | 5 + 35 files changed, 280 insertions(+), 145 deletions(-) diff --git a/contrib/search_api_views/includes/handler_argument.inc b/contrib/search_api_views/includes/handler_argument.inc index f3a97886..a11a662b 100644 --- a/contrib/search_api_views/includes/handler_argument.inc +++ b/contrib/search_api_views/includes/handler_argument.inc @@ -1,5 +1,10 @@ getHierarchicalFields(); } /** - * Display a form for configuring this callback. - * - * @return array - * A form array for configuring this callback, or FALSE if no configuration - * is possible. + * {@inheritdoc} */ public function configurationForm() { $options = $this->getHierarchicalFields(); @@ -54,19 +51,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } /** - * Submit callback for the form returned by configurationForm(). - * - * This method should both return the new options and set them internally. - * - * @param array $form - * The form returned by configurationForm(). - * @param array $values - * The part of the $form_state['values'] array corresponding to this form. - * @param array $form_state - * The complete form state. - * - * @return array - * The new options array for this callback. + * {@inheritdoc} */ public function configurationFormSubmit(array $form, array &$values, array &$form_state) { // Change the saved type of fields in the index, if necessary. @@ -102,19 +87,11 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } /** - * 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. + * {@inheritdoc} */ public function alterItems(array &$items) { if (empty($this->options['fields'])) { - return array(); + return; } foreach ($items as $item) { $wrapper = $this->index->entityWrapper($item, FALSE); @@ -137,16 +114,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } /** - * Declare the properties that are (or can be) added to items with this - * callback. If a property with this name already exists for an entity it - * will be overridden, so keep a clear namespace by prefixing the properties - * with the module name if this is not desired. - * - * @see hook_entity_property_info() - * - * @return array - * Information about all additional properties, as specified by - * hook_entity_property_info() (only the inner "properties" array). + * {@inheritdoc} */ public function propertyInfo() { if (empty($this->options['fields'])) { @@ -188,7 +156,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } /** - * Helper method for finding all hierarchical fields of an index's type. + * Finds all hierarchical fields for the current index. * * @return array * An array containing all hierarchical fields of the index, structured as diff --git a/includes/callback_add_url.inc b/includes/callback_add_url.inc index 097fd41e..630df4b4 100644 --- a/includes/callback_add_url.inc +++ b/includes/callback_add_url.inc @@ -1,5 +1,10 @@ &$item) { diff --git a/includes/datasource.inc b/includes/datasource.inc index f293fa70..10b29ef4 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -129,7 +129,7 @@ interface SearchApiDataSourceControllerInterface { * items were already present, these should also be set to "changed" and not * be inserted again. * - * @param array $indexes + * @param SearchApiIndex[] $indexes * The SearchApiIndex objects for which item tracking should be initialized. * * @throws SearchApiDataSourceException @@ -142,7 +142,7 @@ interface SearchApiDataSourceControllerInterface { * * The tracking tables of the given indexes should be completely cleared. * - * @param array $indexes + * @param SearchApiIndex[] $indexes * The SearchApiIndex objects for which item tracking should be stopped. * * @throws SearchApiDataSourceException @@ -155,7 +155,7 @@ interface SearchApiDataSourceControllerInterface { * * @param array $item_ids * The IDs of new items to track. - * @param array $indexes + * @param SearchApiIndex[] $indexes * The indexes for which items should be tracked. * * @throws SearchApiDataSourceException @@ -172,7 +172,7 @@ interface SearchApiDataSourceControllerInterface { * @param array|false $item_ids * Either an array with the IDs of the changed items. Or FALSE to mark all * items as changed for the given indexes. - * @param array $indexes + * @param SearchApiIndex[] $indexes * The indexes for which the change should be tracked. * @param bool $dequeue * (deprecated) If set to TRUE, also change the status of queued items. @@ -224,7 +224,7 @@ interface SearchApiDataSourceControllerInterface { * * @param array $item_ids * The IDs of the removed items. - * @param array $indexes + * @param SearchApiIndex[] $indexes * The indexes for which the deletions should be tracked. * * @throws SearchApiDataSourceException diff --git a/includes/exception.inc b/includes/exception.inc index 4e7a0c83..f69ddd59 100644 --- a/includes/exception.inc +++ b/includes/exception.inc @@ -1,5 +1,10 @@ stopwords = array_flip(array_merge($file_words, $form_words)); return $this->stopwords; } -} \ No newline at end of file +} diff --git a/includes/processor_tokenizer.inc b/includes/processor_tokenizer.inc index f3972266..14834df0 100644 --- a/includes/processor_tokenizer.inc +++ b/includes/processor_tokenizer.inc @@ -1,5 +1,10 @@ server][$index->machine_name] = $index; @@ -258,15 +261,16 @@ function search_api_admin_add_server(array $form, array &$form_state) { } /** - * AJAX callback that just returns the "options" array of the already built form - * array. + * Form AJAX handler for search_api_admin_add_server(). + * + * Just returns the "options" array of the already built form array. */ function search_api_admin_add_server_ajax_callback(array $form, array &$form_state) { return $form['options']; } /** - * Form validation callback for adding a server. + * Form validation handler for adding a server. * * Validates the machine name and calls the service class' validation handler. */ @@ -297,7 +301,7 @@ function search_api_admin_add_server_validate(array $form, array &$form_state) { } /** - * Form submit callback for adding a server. + * Form submission handler for adding a server. */ function search_api_admin_add_server_submit(array $form, array &$form_state) { form_state_values_clean($form_state); @@ -338,10 +342,8 @@ function search_api_admin_item_title($object) { * @param SearchApiServer $server * The server to display. * @param string|null $action - * One of 'enable', 'disable', 'delete'; or NULL if the server is only viewed. - * - * @return array - * A render array for displaying information about a server. + * (optional) An action to execute for the server. One of 'enable', 'disable' + * or 'clear'. * * @see search_api_menu() */ @@ -455,10 +457,15 @@ function theme_search_api_server(array $variables) { } /** - * Edit a server's settings. + * Form constructor for editing a server's settings. * * @param SearchApiServer $server * The server to edit. + * + * @ingroup forms + * + * @see search_api_admin_server_edit_validate() + * @see search_api_admin_server_edit_submit() */ function search_api_admin_server_edit(array $form, array &$form_state, SearchApiServer $server) { $form_state['server'] = $server; @@ -512,14 +519,18 @@ function search_api_admin_server_edit(array $form, array &$form_state, SearchApi } /** - * Validation function for search_api_admin_server_edit. + * Form validation handler for search_api_admin_server_edit(). + * + * @see search_api_admin_server_edit_submit() */ function search_api_admin_server_edit_validate(array $form, array &$form_state) { $form_state['server']->configurationFormValidate($form['options']['form'], $form_state['values']['options']['form'], $form_state); } /** - * Submit function for search_api_admin_server_edit. + * Form submission handler for search_api_admin_server_edit(). + * + * @see search_api_admin_server_edit_validate() */ function search_api_admin_server_edit_submit(array $form, array &$form_state) { form_state_values_clean($form_state); @@ -561,7 +572,12 @@ function search_api_admin_form_delete_submit($form, &$form_state) { } /** - * Form callback showing a form for adding an index. + * Form constructor for adding an index. + * + * @ingroup forms + * + * @see search_api_admin_add_index_validate() + * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index(array $form, array &$form_state) { drupal_set_title(t('Add index')); @@ -648,7 +664,9 @@ function search_api_admin_add_index(array $form, array &$form_state) { } /** - * Validation callback for search_api_admin_add_index. + * Form validation handler for search_api_admin_add_index(). + * + * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index_validate(array $form, array &$form_state) { $name = $form_state['values']['machine_name']; @@ -664,7 +682,9 @@ function search_api_admin_add_index_validate(array $form, array &$form_state) { } /** - * Submit callback for search_api_admin_add_index. + * Form submission handler for search_api_admin_add_index(). + * + * @see search_api_admin_add_index_validate() */ function search_api_admin_add_index_submit(array $form, array &$form_state) { form_state_values_clean($form_state); @@ -685,8 +705,10 @@ function search_api_admin_add_index_submit(array $form, array &$form_state) { * @param SearchApiIndex $index * The index to display. * @param string|null $action - * (optional) An action to execute for the index. Either "enable" or - * "disable". For "disable", a confirm dialog will be shown. + * (optional) An action to execute for the index. One of "reindex", "clear", + * "enable" or "disable". For "disable", a confirm dialog will be shown. + * + * @see search_api_menu() */ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { if (!empty($action)) { @@ -865,7 +887,9 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea } /** - * Validation function for search_api_admin_index_status_form. + * Form validation handler for search_api_admin_index_status_form(). + * + * @see search_api_admin_index_status_form_submit() */ function search_api_admin_index_status_form_validate(array $form, array &$form_state) { if ($form_state['values']['op'] == t('Index now') && !$form_state['values']['limit']) { @@ -874,7 +898,9 @@ function search_api_admin_index_status_form_validate(array $form, array &$form_s } /** - * Submit function for search_api_admin_index_status_form. + * Form submission handler for search_api_admin_index_status_form(). + * + * @see search_api_admin_index_status_form_validate() */ function search_api_admin_index_status_form_submit(array $form, array &$form_state) { $redirect = &$form_state['redirect']; @@ -923,10 +949,14 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta } /** - * Edit an index' settings. + * Form constructor for editing an index's settings. * * @param SearchApiIndex $index * The index to edit. + * + * @ingroup forms + * + * @see search_api_admin_index_edit_submit() */ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) { $form_state['index'] = $index; @@ -1010,7 +1040,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI } /** - * Submit callback for search_api_admin_index_edit. + * Form submission handler for search_api_admin_index_edit(). */ function search_api_admin_index_edit_submit(array $form, array &$form_state) { form_state_values_clean($form_state); @@ -1034,8 +1064,13 @@ function search_api_admin_index_edit_submit(array $form, array &$form_state) { * * @param SearchApiIndex $index * The index to edit. + * + * @ingroup forms + * + * @see search_api_admin_index_workflow_validate() + * @see search_api_admin_index_workflow_submit() */ -// Copied from filter_admin_format_form +// Copied from filter_admin_format_form() function search_api_admin_index_workflow(array $form, array &$form_state, SearchApiIndex $index) { $callback_info = search_api_get_alter_callbacks(); $processor_info = search_api_get_processors(); @@ -1275,7 +1310,9 @@ function theme_search_api_admin_item_order(array $variables) { } /** - * Validation callback for search_api_admin_index_workflow. + * Form validation handler for search_api_admin_index_workflow(). + * + * @see search_api_admin_index_workflow_submit() */ function search_api_admin_index_workflow_validate(array $form, array &$form_state) { // Call validation functions. @@ -1292,7 +1329,9 @@ function search_api_admin_index_workflow_validate(array $form, array &$form_stat } /** - * Submit callback for search_api_admin_index_workflow. + * Form submission handler for search_api_admin_index_workflow(). + * + * @see search_api_admin_index_workflow_validate() */ function search_api_admin_index_workflow_submit(array $form, array &$form_state) { $values = $form_state['values']; @@ -1327,7 +1366,8 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) $type = $field['type']; $inner = search_api_extract_inner_type($type); if ($inner != 'token' && empty($types[$inner])) { - // Someone apparently added a structure or entity as a property in a data-alter callback. + // Someone apparently added a structure or entity as a property in + // a data alteration. continue; } if ($inner == 'token' || (search_api_is_text_type($inner) && !empty($field['options list']))) { @@ -1385,7 +1425,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) /** * Sort callback sorting array elements by their "weight" key, if present. * - * @see element_sort + * @see element_sort() */ function search_api_admin_element_compare($a, $b) { $a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0; @@ -1397,10 +1437,11 @@ function search_api_admin_element_compare($a, $b) { } /** - * Select the indexed fields. + * Form constructor for setting the indexed fields. * - * @param SearchApiIndex $index - * The index to edit. + * @ingroup forms + * + * @see search_api_admin_index_fields_submit() */ function search_api_admin_index_fields(array $form, array &$form_state, SearchApiIndex $index) { $options = $index->getFields(FALSE, TRUE); @@ -1594,14 +1635,17 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp $added[$key] = TRUE; } - // Then we walk through all properties and look if they are already contained in one of the arrays. - // Since this uses an iterative instead of a recursive approach, it is a bit complicated, with three arrays tracking the current depth. + // Then we walk through all properties and look if they are already contained + // in one of the arrays. Since this uses an iterative instead of a recursive + // approach, it is a bit complicated, with three arrays tracking the current + // depth. - // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper + // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user + // wrapper $wrappers = array('' => $wrapper); // Display names for the prefixes $prefix_names = array('' => ''); - // The list nesting level for entities with a certain prefix + // The list nesting level for entities with a certain prefix $nesting_levels = array('' => 0); $types = search_api_default_field_types(); @@ -1627,7 +1671,8 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp // We hide the complexity of multi-valued types from the user here. $type = search_api_extract_inner_type($info['type']); // Treat Entity API type "token" as our "string" type. - // Also let text fields with limited options be of type "string" by default. + // Also let text fields with limited options be of type "string" by + // default. if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) { // Inner type is changed to "string". $type = 'string'; @@ -1742,7 +1787,7 @@ function theme_search_api_admin_fields_table($variables) { } /** - * Submit function for search_api_admin_index_fields. + * Form submission handler for search_api_admin_index_fields(). */ function search_api_admin_index_fields_submit(array $form, array &$form_state) { $index = $form_state['index']; @@ -1806,14 +1851,14 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { } /** - * Helper function for displaying a generic confirmation form. + * Form constructor for a generic confirmation form. * * @param $type * The type of entity (not the real "entity type"). Either "server" or * "index". * @param $action - * The action that would be executed for this entity after confirming. Either - * "disable" or "delete". + * The action that would be executed for this entity after confirming. One of + * "reindex" ("index" type only), "clear", "disable" or "delete". * @param Entity $entity * The entity for which the action would be performed. Must have a "name" * property. diff --git a/search_api.admin.js b/search_api.admin.js index dcb3a77b..9ff40ae0 100644 --- a/search_api.admin.js +++ b/search_api.admin.js @@ -1,10 +1,14 @@ +/** + * @file + * Javascript enhancements for the Search API admin pages. + */ -// Copied from filter.admin.js (function ($) { /** * Allows the re-ordering of enabled data alterations and processors. */ +// Copied from filter.admin.js Drupal.behaviors.searchApiStatus = { attach: function (context, settings) { $('.search-api-status-wrapper input.form-checkbox', context).once('search-api-status', function () { diff --git a/search_api.module b/search_api.module index c52eca4f..5ab5c669 100644 --- a/search_api.module +++ b/search_api.module @@ -1050,9 +1050,9 @@ function search_api_search_api_processor_info() { /** * Inserts new unindexed items for all indexes on the specified type. * - * @param $type + * @param string $type * The item type of the new items. - * @param array $item_id + * @param array $item_ids * The IDs of the new items. */ function search_api_track_item_insert($type, array $item_ids) { @@ -1328,7 +1328,7 @@ function search_api_server_tasks_delete(array $ids = NULL, SearchApiServer $serv * index and, if a discrepancy is spotted, re-save that index with updated * fields options (thus, of course, also triggering a re-indexing operation). * - * @param array|false $indexes + * @param SearchApiIndex[]|false $indexes * An array of SearchApiIndex objects on which to perform the operation, or * FALSE to perform it on all indexes. */ @@ -1978,17 +1978,20 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu } /** - * Utility function for determining whether a field of the given type contains - * text data. + * Determines whether a field of the given type contains text data. * - * @param $type - * A string containing the type to check. + * Can also be used to find other types. + * + * @param string $type + * The type for which to check. * @param array $allowed * Optionally, an array of allowed types. * * @return * TRUE if $type is either one of the specified types, or a list of such * values. FALSE otherwise. + * + * @see search_api_extract_inner_type() */ function search_api_is_text_type($type, array $allowed = array('text')) { return array_search(search_api_extract_inner_type($type), $allowed) !== FALSE; @@ -2270,7 +2273,7 @@ function search_api_server_load($id, $reset = FALSE) { * @param bool $reset * Whether to reset the internal entity_load cache. * - * @return array + * @return SearchApiServer[] * An array of server objects keyed by machine name. */ function search_api_server_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) { @@ -2443,7 +2446,7 @@ function search_api_index_load($id, $reset = FALSE) { * @param bool $reset * Whether to reset the internal entity_load cache. * - * @return array + * @return SearchApiIndex[] * An array of index objects keyed by machine name. */ function search_api_index_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) { @@ -2730,18 +2733,21 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ } /** - * Create and set a batch for indexing items. + * Creates and sets a batch for indexing items. * * @param SearchApiIndex $index * The index for which items should be indexed. - * @param $batch_size + * @param int $batch_size * Number of items to index per batch. - * @param $limit - * Maximum number of items to index. - * @param $remaining + * @param int $limit + * Maximum number of items to index. Negative values mean "no limit". + * @param int $remaining * Remaining items to index. - * @param $drush + * @param bool $drush * Boolean specifying whether this was called from drush or not. + * + * @return bool + * Whether the batch was created and set successfully. */ function _search_api_batch_indexing_create(SearchApiIndex $index, $batch_size, $limit, $remaining, $drush = FALSE) { if ($limit !== 0 && $batch_size !== 0) { diff --git a/tests/search_api_test.module b/tests/search_api_test.module index 00a2028d..1b227f3d 100644 --- a/tests/search_api_test.module +++ b/tests/search_api_test.module @@ -1,5 +1,10 @@ Date: Tue, 19 Nov 2013 08:49:11 +0100 Subject: [PATCH 058/278] Follow-up to #2135363 by drunken monkey: Fixed logic of the "skip result count" option in Views. --- contrib/search_api_views/includes/query.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 4aab9f60..2e7b2af5 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -246,7 +246,7 @@ class SearchApiViewsQuery extends views_plugin_query { // Initialize the pager and let it modify the query to add limits. $view->init_pager(); $this->pager->query(); - $this->query->setOption('skip result count', $this->pager->use_count_query()); + $this->query->setOption('skip result count', !$this->pager->use_count_query()); // Views passes sometimes NULL and sometimes the integer 0 for "All" in a // pager. If set to 0 items, a string "0" is passed. Therefore, we unset From d4a12a94eee9d2e846f2b5afade9f310f527038b Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 25 Nov 2013 09:18:08 +0100 Subject: [PATCH 059/278] Issue #2136019 by drunken monkey: Fixed mapping callback for taxonomy term facets. --- CHANGELOG.txt | 1 + .../search_api_facetapi.module | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ebd46ed9..81362604 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2136019 by drunken monkey: Fixed mapping callback for taxonomy term facets. - #2128001 by drunken monkey: Fixed the logic of the "contains none of these words" fulltext operator. - #2128947 by stBorchert, drunken monkey: Fixed facet handling for multiple diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index 64aec586..29a2dbd5 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -92,7 +92,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) { // other modules. $type_settings = array( 'taxonomy_term' => array( - 'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy', + 'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy', ), 'date' => array( 'query type' => 'date', @@ -226,6 +226,26 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) { return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name); } +/** + * Gets hierarchy information for taxonomy terms. + * + * Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info(). + * + * Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that + * our special "!" value is not passed. + * + * @param array $values + * An array containing the term IDs. + * + * @return array + * An associative array mapping term IDs to parent IDs (where parents could be + * found). + */ +function search_api_facetapi_get_taxonomy_hierarchy(array $values) { + $values = array_filter($values, 'is_numeric'); + return $values ? facetapi_get_taxonomy_hierarchy($values) : array(); +} + /** * Map callback for all search_api facet fields. * From b7e8b15cb008536da5bfcf7796dc0d38f004952b Mon Sep 17 00:00:00 2001 From: kscheirer Date: Tue, 26 Nov 2013 09:33:52 +0100 Subject: [PATCH 060/278] Issue #2134509 by kscheirer, drunken monkey: Removed unused variables and parameters. --- CHANGELOG.txt | 2 ++ .../plugins/facetapi/adapter.inc | 2 +- contrib/search_api_views/includes/plugin_cache.inc | 2 +- contrib/search_api_views/search_api_views.install | 4 ++-- contrib/search_api_views/search_api_views.module | 2 +- includes/callback_add_hierarchy.inc | 4 ++-- includes/callback_add_url.inc | 2 +- includes/callback_add_viewed_entity.inc | 2 +- includes/datasource.inc | 2 +- includes/processor_highlight.inc | 5 ++--- search_api.admin.inc | 1 - search_api.install | 2 +- search_api.module | 14 +++++++------- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 81362604..0bf3587f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2134509 by kscheirer, drunken monkey: Removed unused variables and + parameters. - #2136019 by drunken monkey: Fixed mapping callback for taxonomy term facets. - #2128001 by drunken monkey: Fixed the logic of the "contains none of these words" fulltext operator. diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index 824350ad..cddcf56d 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -144,7 +144,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { // I suspect that http://drupal.org/node/593658 would help. // For now, just taking the first current search for this index. :-/ foreach (search_api_current_search() as $search) { - list($query, $results) = $search; + list($query) = $search; if ($query->getIndex()->machine_name == $index_id) { $this->current_search = $search; } diff --git a/contrib/search_api_views/includes/plugin_cache.inc b/contrib/search_api_views/includes/plugin_cache.inc index 890c9701..52724a00 100644 --- a/contrib/search_api_views/includes/plugin_cache.inc +++ b/contrib/search_api_views/includes/plugin_cache.inc @@ -98,7 +98,7 @@ class SearchApiViewsCache extends views_plugin_cache_time { // other parameters used in the parent method are already reflected in the // Search API query object we use. if (isset($_GET['exposed_info'])) { - $key_data[$key] = $_GET[$key]; + $key_data['exposed_info'] = $_GET['exposed_info']; } $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data)); diff --git a/contrib/search_api_views/search_api_views.install b/contrib/search_api_views/search_api_views.install index 30a023c3..03e610bf 100644 --- a/contrib/search_api_views/search_api_views.install +++ b/contrib/search_api_views/search_api_views.install @@ -25,7 +25,7 @@ function search_api_views_update_7101() { if (!$table_fields) { return; } - foreach (views_get_all_views() as $name => $view) { + foreach (views_get_all_views() as $view) { if (empty($view->base_table) || empty($table_fields[$view->base_table])) { continue; } @@ -33,7 +33,7 @@ function search_api_views_update_7101() { $fields = $table_fields[$view->base_table]; $change |= _search_api_views_update_7101_helper($view->base_field, $fields); if (!empty($view->display)) { - foreach ($view->display as $key => &$display) { + foreach ($view->display as &$display) { $options = &$display->display_options; if (isset($options['style_options']['grouping'])) { $change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields); diff --git a/contrib/search_api_views/search_api_views.module b/contrib/search_api_views/search_api_views.module index 75fc6273..8a131c2a 100644 --- a/contrib/search_api_views/search_api_views.module +++ b/contrib/search_api_views/search_api_views.module @@ -17,7 +17,7 @@ function search_api_views_views_api() { /** * Implements hook_search_api_index_insert(). */ -function search_api_views_search_api_index_insert(SearchApiIndex $index) { +function search_api_views_search_api_index_insert() { // Make the new index available for views. views_invalidate_cache(); } diff --git a/includes/callback_add_hierarchy.inc b/includes/callback_add_hierarchy.inc index c1b876f4..d69badb6 100644 --- a/includes/callback_add_hierarchy.inc +++ b/includes/callback_add_hierarchy.inc @@ -59,7 +59,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { $fields = &$this->index->options['fields']; $previous = drupal_map_assoc($this->options['fields']); foreach ($values['fields'] as $field) { - list($key, $prop) = explode(':', $field); + list($key) = explode(':', $field); if (empty($previous[$field]) && isset($fields[$key]['type'])) { $fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>'; $change = TRUE; @@ -67,7 +67,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } $new = drupal_map_assoc($values['fields']); foreach ($previous as $field) { - list($key, $prop) = explode(':', $field); + list($key) = explode(':', $field); if (empty($new[$field]) && isset($fields[$key]['type'])) { $w = $this->index->entityWrapper(NULL, FALSE); if (isset($w->$key)) { diff --git a/includes/callback_add_url.inc b/includes/callback_add_url.inc index 630df4b4..cc76b33a 100644 --- a/includes/callback_add_url.inc +++ b/includes/callback_add_url.inc @@ -11,7 +11,7 @@ class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback { public function alterItems(array &$items) { - foreach ($items as $id => &$item) { + foreach ($items as &$item) { $url = $this->index->datasource()->getItemUrl($item); if (!$url) { $item->search_api_url = NULL; diff --git a/includes/callback_add_viewed_entity.inc b/includes/callback_add_viewed_entity.inc index a9432249..06b05c38 100644 --- a/includes/callback_add_viewed_entity.inc +++ b/includes/callback_add_viewed_entity.inc @@ -69,7 +69,7 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback { $type = $this->index->getEntityType(); $mode = empty($this->options['mode']) ? 'full' : $this->options['mode']; - foreach ($items as $id => &$item) { + foreach ($items as &$item) { // Since we can't really know what happens in entity_view() and render(), // we use try/catch. This will at least prevent some errors, even though // it's no protection against fatal errors and the like. diff --git a/includes/datasource.inc b/includes/datasource.inc index 10b29ef4..cf0507fd 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -507,7 +507,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou // will mostly be called with only one index. foreach ($indexes as $index) { $this->checkIndex($index); - $query = db_delete($this->table) + db_delete($this->table) ->condition($this->indexIdColumn, $index->id) ->execute(); } diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index a7ed4dbe..ac33cff8 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -166,10 +166,10 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { protected function getFulltextFields(array &$results, $i, $load = TRUE) { global $language; $data = array(); - // Act as if $load is TRUE if we have a loaded item. - $load |= !empty($result['entity']); $result = &$results[$i]; + // Act as if $load is TRUE if we have a loaded item. + $load |= !empty($result['entity']); $result += array('fields' => array()); $fulltext_fields = $this->index->getFulltextFields(); // We only need detailed fields data if $load is TRUE. @@ -294,7 +294,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { // If the sum of all fragments is too short, we look for second occurrences. $ranges = array(); $included = array(); - $foundkeys = array(); $length = 0; $workkeys = $keys; while ($length < $this->options['excerpt_length'] && count($workkeys)) { diff --git a/search_api.admin.inc b/search_api.admin.inc index bdb555cf..bc950e47 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1340,7 +1340,6 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) $index = $form_state['index']; $options = empty($index->options) ? array() : $index->options; - $fields_set = !empty($options['fields']); // Store callback and processor settings. foreach ($form_state['callbacks'] as $name => $callback) { diff --git a/search_api.install b/search_api.install index 9a113750..44e740f9 100644 --- a/search_api.install +++ b/search_api.install @@ -651,7 +651,7 @@ function search_api_update_7106() { $callbacks['search_api_alter_add_aggregation'] = $callbacks['search_api_alter_add_fulltext']; unset($callbacks['search_api_alter_add_fulltext']); if (!empty($callbacks['search_api_alter_add_aggregation']['settings']['fields'])) { - foreach ($callbacks['search_api_alter_add_aggregation']['settings']['fields'] as $field => &$info) { + foreach ($callbacks['search_api_alter_add_aggregation']['settings']['fields'] as &$info) { if (!isset($info['type'])) { $info['type'] = 'fulltext'; } diff --git a/search_api.module b/search_api.module index 5ab5c669..0dcfa3fb 100644 --- a/search_api.module +++ b/search_api.module @@ -699,7 +699,7 @@ function search_api_search_api_index_delete(SearchApiIndex $index) { * * Adds dependency information for exported servers. */ -function search_api_features_export_alter(&$export, $module_name) { +function search_api_features_export_alter(&$export) { if (isset($export['features']['search_api_server'])) { // Get a list of the modules that provide storage engines. $hook = 'search_api_service_info'; @@ -874,7 +874,7 @@ function search_api_entity_delete($entity, $type) { * Recalculates fields settings if the cardinality of the field has changed from * or to 1. */ -function search_api_field_update_field($field, $prior_field, $has_data) { +function search_api_field_update_field($field, $prior_field) { $before = $prior_field['cardinality']; $after = $field['cardinality']; if ($before != $after && ($before == 1 || $after == 1)) { @@ -918,7 +918,7 @@ function search_api_search_api_item_type_info() { /** * Implements hook_modules_enabled(). */ -function search_api_modules_enabled(array $modules) { +function search_api_modules_enabled() { // New modules might offer additional item types or service classes, // invalidating the cached information. drupal_static_reset('search_api_get_item_type_info'); @@ -928,7 +928,7 @@ function search_api_modules_enabled(array $modules) { /** * Implements hook_modules_disabled(). */ -function search_api_modules_disabled(array $modules) { +function search_api_modules_disabled() { // The disabled modules might have offered item types or service classes, // invalidating the cached information. drupal_static_reset('search_api_get_item_type_info'); @@ -2124,13 +2124,13 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields // If $wrapper is a list of entities, we have to aggregate their field values. $wrapper_info = $wrapper->info(); if (search_api_is_list_type($wrapper_info['type'])) { - foreach ($fields as $field => &$info) { + foreach ($fields as &$info) { $info['value'] = array(); $info['original_type'] = $info['type']; } unset($info); try { - foreach ($wrapper as $i => $w) { + foreach ($wrapper as $w) { $nested_fields = search_api_extract_fields($w, $fields, $value_options); foreach ($nested_fields as $field => $info) { if (isset($info['value'])) { @@ -2194,7 +2194,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields } } else { - foreach ($nested_fields as $field => &$info) { + foreach ($nested_fields as &$info) { $info['value'] = NULL; $info['original_type'] = $info['type']; } From 4541d6b661fbaf3a4a67d8edc3195bf55c6aecb4 Mon Sep 17 00:00:00 2001 From: khiminrm Date: Tue, 26 Nov 2013 09:37:14 +0100 Subject: [PATCH 061/278] Issue #2143659 by khiminrm: Fixed typo in update function 7116. --- CHANGELOG.txt | 1 + search_api.install | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0bf3587f..098f8c26 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2143659 by khiminrm: Fixed typo in update function 7116. - #2134509 by kscheirer, drunken monkey: Removed unused variables and parameters. - #2136019 by drunken monkey: Fixed mapping callback for taxonomy term facets. diff --git a/search_api.install b/search_api.install index 44e740f9..1d3b2043 100644 --- a/search_api.install +++ b/search_api.install @@ -942,7 +942,7 @@ function search_api_update_7116() { foreach ($select->execute() as $index) { $index_ids[] = $index->machine_name; $tasks[] = array( - 'server_id' => $$index->server, + 'server_id' => $index->server, 'type' => 'removeIndex', 'index_id' => $index->machine_name, ); From ce8a47f990fb0e3f710328f865ae8430176ec8ca Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 27 Nov 2013 08:27:03 +0100 Subject: [PATCH 062/278] Issue #2139215 by drunken monkey: Fixed $context parameter of batch callback. --- CHANGELOG.txt | 1 + search_api.module | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 098f8c26..28fe3721 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2139215 by drunken monkey: Fixed $context parameter of batch callback. - #2143659 by khiminrm: Fixed typo in update function 7116. - #2134509 by kscheirer, drunken monkey: Removed unused variables and parameters. diff --git a/search_api.module b/search_api.module index 0dcfa3fb..a1e1c898 100644 --- a/search_api.module +++ b/search_api.module @@ -2785,10 +2785,10 @@ function _search_api_batch_indexing_create(SearchApiIndex $index, $batch_size, $ * Maximum number of items to index. * @param boolean $drush * Boolean specifying whether this was called from drush or not. - * @param array $context - * The batch context. + * @param $context + * An array (or object implementing ArrayAccess) containing the batch context. */ -function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, $limit, $drush = FALSE, array &$context) { +function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, $limit, $drush = FALSE, &$context) { // Persistent data among batch runs. if (!isset($context['sandbox']['limit'])) { $context['sandbox']['limit'] = $limit; From b91d5f5978a3d90b66bb0709d6a2a1c9713fc680 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 30 Nov 2013 18:12:33 +0100 Subject: [PATCH 063/278] Doc comment fixes for the implemented entity CRUD hooks. --- search_api.module | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/search_api.module b/search_api.module index a1e1c898..431df9f9 100644 --- a/search_api.module +++ b/search_api.module @@ -801,13 +801,11 @@ function search_api_system_info_alter(&$info, $file, $type) { /** * Implements hook_entity_insert(). * - * Marks the new item as to-index for all indexes on entities of the specified - * type. + * This is implemented on behalf of the SearchApiEntityDataSourceController + * datasource controller and calls search_api_track_item_insert() for the + * inserted items. * - * @param $entity - * The new entity. - * @param $type - * The entity's type. + * @see search_api_search_api_item_type_info() */ function search_api_entity_insert($entity, $type) { // When inserting a new search index, the new index was already inserted into @@ -827,12 +825,11 @@ function search_api_entity_insert($entity, $type) { /** * Implements hook_entity_update(). * - * Marks the item as changed for all indexes on entities of the specified type. + * This is implemented on behalf of the SearchApiEntityDataSourceController + * datasource controller and calls search_api_track_item_change() for the + * updated items. * - * @param $entity - * The updated entity. - * @param $type - * The entity's type. + * @see search_api_search_api_item_type_info() */ function search_api_entity_update($entity, $type) { // We only react on entity operations for types with property information, as @@ -849,12 +846,11 @@ function search_api_entity_update($entity, $type) { /** * Implements hook_entity_delete(). * - * Removes the item from the tracking table and deletes it from all indexes. + * This is implemented on behalf of the SearchApiEntityDataSourceController + * datasource controller and calls search_api_track_item_delete() for the + * deleted items. * - * @param $entity - * The updated entity. - * @param $type - * The entity's type. + * @see search_api_search_api_item_type_info() */ function search_api_entity_delete($entity, $type) { // We only react on entity operations for types with property information, as From 5cf7cc5eebaebac24a8fe1a4841c6049b4419a0d Mon Sep 17 00:00:00 2001 From: azinck Date: Wed, 4 Dec 2013 08:49:08 +0100 Subject: [PATCH 064/278] Issue #1925114 by azinck: Fixed Views Facet Block integration with Panels. --- CHANGELOG.txt | 1 + .../includes/display_facet_block.inc | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 28fe3721..79f4e3e5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1925114 by azinck: Fixed Views Facet Block integration with Panels. - #2139215 by drunken monkey: Fixed $context parameter of batch callback. - #2143659 by khiminrm: Fixed typo in update function 7116. - #2134509 by kscheirer, drunken monkey: Removed unused variables and diff --git a/contrib/search_api_views/includes/display_facet_block.inc b/contrib/search_api_views/includes/display_facet_block.inc index 39d256b3..35ad14f5 100644 --- a/contrib/search_api_views/includes/display_facet_block.inc +++ b/contrib/search_api_views/includes/display_facet_block.inc @@ -151,11 +151,9 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { } } - public function execute() { - if (substr($this->view->base_table, 0, 17) != 'search_api_index_') { - form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.')); - return NULL; - } + public function query(){ + parent::query(); + $facet_field = $this->get_option('facet_field'); if (!$facet_field) { return NULL; @@ -165,7 +163,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { if (!$base_path) { $base_path = $_GET['q']; } - $this->view->build(); + $limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page']; $query_options = &$this->view->query->getOptions(); if (!$this->get_option('hide_block')) { @@ -179,6 +177,17 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { } $query_options['search_api_base_path'] = $base_path; $this->view->query->range(0, 0); + } + + public function render() { + if (substr($this->view->base_table, 0, 17) != 'search_api_index_') { + form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.')); + return NULL; + } + $facet_field = $this->get_option('facet_field'); + if (!$facet_field) { + return NULL; + } $this->view->execute(); @@ -229,7 +238,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { // Initializes variables passed to theme hook. $variables = array( 'text' => $name, - 'path' => $base_path, + 'path' => $this->view->query->getOption('search_api_base_path'), 'count' => $term['count'], 'options' => array( 'attributes' => array('class' => 'facetapi-inactive'), @@ -249,10 +258,16 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { return NULL; } - $info['content']['facets'] = array( + return array( + 'facets' => array( '#theme' => 'item_list', '#items' => $facets, + ) ); + } + + public function execute(){ + $info['content'] = $this->render(); $info['content']['more'] = $this->render_more_link(); $info['subject'] = filter_xss_admin($this->view->get_title()); return $info; From 328d4c63b94e8c6b1ddd38c782113e148694dd73 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 5 Dec 2013 08:04:10 +0100 Subject: [PATCH 065/278] Issue #2139239 by drunken monkey: Fixed highlighting for the last word of a field. --- CHANGELOG.txt | 1 + includes/processor_highlight.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 79f4e3e5..b8ee659f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2139239 by drunken monkey: Fixed highlighting for the last word of a field. - #1925114 by azinck: Fixed Views Facet Block integration with Panels. - #2139215 by drunken monkey: Fixed $context parameter of batch callback. - #2143659 by khiminrm: Fixed typo in update function 7116. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index ac33cff8..2c7f3676 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -395,8 +395,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { */ protected function highlightField($text, array $keys) { $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; - $text = preg_replace('/' . self::$boundary . '(' . implode('|', $keys) . ')' . self::$boundary . '/iu', $replace, ' ' . $text); - return substr($text, 1); + $text = preg_replace('/' . self::$boundary . '(' . implode('|', $keys) . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); + return substr($text, 1, -1); } } From 88c559f24010dd4cb5d115fa14e8840e2dfa44bd Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Dec 2013 08:21:37 +0100 Subject: [PATCH 066/278] Issue #2100671 by drunken monkey: Fixed stopwords processor to ignore missing stopwords. --- CHANGELOG.txt | 2 ++ includes/processor_stopwords.inc | 12 +++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b8ee659f..a1c12cd6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2100671 by drunken monkey: Fixed stopwords processor to ignore missing + stopwords. - #2139239 by drunken monkey: Fixed highlighting for the last word of a field. - #1925114 by azinck: Fixed Views Facet Block integration with Panels. - #2139215 by drunken monkey: Fixed $context parameter of batch callback. diff --git a/includes/processor_stopwords.inc b/includes/processor_stopwords.inc index 8abb4b00..dddb7969 100644 --- a/includes/processor_stopwords.inc +++ b/includes/processor_stopwords.inc @@ -26,8 +26,7 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { ), 'file' => array( '#type' => 'textfield', - '#title' => t('Stopwords file URI'), - '#title' => t('Enter the URI of your stopwords.txt file'), + '#title' => t('Stopwords file'), '#description' => t('This must be a stream-type description like public://stopwords/stopwords.txt or http://example.com/stopwords.txt or private://stopwords.txt.'), ), 'stopwords' => array( @@ -48,13 +47,8 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { public function configurationFormValidate(array $form, array &$values, array &$form_state) { parent::configurationFormValidate($form, $values, $form_state); - $stopwords = trim($values['stopwords']); $uri = $values['file']; - if (empty($stopwords) && empty($uri)) { - $el = $form['file']; - form_error($el, $el['#title'] . ': ' . t('At stopwords file or words are required.')); - } - if (!empty($uri) && !file_get_contents($uri)) { + if (!empty($uri) && !@file_get_contents($uri)) { $el = $form['file']; form_error($el, t('Stopwords file') . ': ' . t('The file %uri is not readable or does not exist.', array('%uri' => $uri))); } @@ -62,7 +56,7 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { public function process(&$value) { $stopwords = $this->getStopWords(); - if (empty($stopwords) && !is_string($value)) { + if (empty($stopwords) || !is_string($value)) { return; } $words = preg_split('/\s+/', $value); From 7167bdcd7d442c6d80abe9a2df7bf57ffdaaa3e0 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Dec 2013 08:22:33 +0100 Subject: [PATCH 067/278] Issue #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too. --- CHANGELOG.txt | 1 + includes/query.inc | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a1c12cd6..32ce40f9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too. - #2100671 by drunken monkey: Fixed stopwords processor to ignore missing stopwords. - #2139239 by drunken monkey: Fixed highlighting for the last word of a field. diff --git a/includes/query.inc b/includes/query.inc index 41913778..e9ccc207 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -804,6 +804,13 @@ class SearchApiQuery implements SearchApiQueryInterface { } } + /** + * Implements the magic __clone() method to clone the filter, too. + */ + public function __clone() { + $this->filter = clone $this->filter; + } + } /** @@ -992,4 +999,15 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { return $this->tags; } + /** + * Implements the magic __clone() method to clone nested filters, too. + */ + public function __clone() { + foreach ($this->filters as $i => $filter) { + if (is_object($filter)) { + $this->filters[$i] = clone $filter; + } + } + } + } From 456dd8dc7a2c79148ac2ae619e7cd510c2d89080 Mon Sep 17 00:00:00 2001 From: sirtet Date: Mon, 9 Dec 2013 08:44:48 +0100 Subject: [PATCH 068/278] Issue #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c. --- CHANGELOG.txt | 1 + search_api.drush.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 32ce40f9..7a124cf5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c. - #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too. - #2100671 by drunken monkey: Fixed stopwords processor to ignore missing stopwords. diff --git a/search_api.drush.inc b/search_api.drush.inc index 67e5f855..e1841e8f 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -99,8 +99,8 @@ function search_api_drush_command() { 'examples' => array( 'drush searchapi-clear' => dt('Clear all search indexes.'), 'drush sapi-c' => dt('Alias to clear all search indexes.'), - 'drush sapi-r 1' => dt('Clear the search index with the ID !id.', array('!id' => 1)), - 'drush sapi-r default_node_index' => dt('Clear the search index with the machine name !name.', array('!name' => 'default_node_index')), + 'drush sapi-c 1' => dt('Clear the search index with the ID !id.', array('!id' => 1)), + 'drush sapi-c default_node_index' => dt('Clear the search index with the machine name !name.', array('!name' => 'default_node_index')), ), 'arguments' => array( 'index_id' => dt('The numeric ID or machine name of an index.'), From 3007d21fd5599c913b4c0a37431e297ea490cac7 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Dec 2013 09:07:36 +0100 Subject: [PATCH 069/278] Issue #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs. --- CHANGELOG.txt | 1 + includes/server_entity.inc | 19 ++ includes/service.inc | 24 ++ search_api.admin.css | 26 +- search_api.admin.inc | 640 ++++++++++++++++++++++++++----------- search_api.api.php | 3 +- search_api.module | 118 ++++++- search_api.test | 57 ++-- 8 files changed, 667 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7a124cf5..23fa74cf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs. - #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c. - #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too. - #2100671 by drunken monkey: Fixed stopwords processor to ignore missing diff --git a/includes/server_entity.inc b/includes/server_entity.inc index b13d7328..be2a568c 100644 --- a/includes/server_entity.inc +++ b/includes/server_entity.inc @@ -374,4 +374,23 @@ class SearchApiServer extends Entity { return $this->proxy->search($query); } + /** + * Retrieves additional information for the server, if available. + * + * Retrieving such information is only supported if the service class supports + * the "search_api_service_extra" feature. + * + * @return array + * An array containing additional, service class-specific information about + * the server. + * + * @see SearchApiAbstractService::getExtraInformation() + */ + public function getExtraInformation() { + if ($this->proxy->supportsFeature('search_api_service_extra')) { + return $this->proxy->getExtraInformation(); + } + return array(); + } + } diff --git a/includes/service.inc b/includes/service.inc index 6442f9b8..b8d8ca88 100644 --- a/includes/service.inc +++ b/includes/service.inc @@ -370,6 +370,30 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { return $output ? "
    \n$output
    " : ''; } + /** + * Returns additional, service-specific information about this server. + * + * If a service class implements this method and supports the + * "search_api_service_extra" option, this method will be used to add extra + * information to the server's "View" tab. + * + * In the default theme implementation this data will be output in a table + * with two columns along with other, generic information about the server. + * + * @return array + * An array of additional server information, with each piece of information + * being an associative array with the following keys: + * - label: The human-readable label for this data. + * - info: The information, as HTML. + * - status: (optional) The status associated with this information. One of + * "info", "ok", "warning" or "error". Defaults to "info". + * + * @see supportsFeature() + */ + public function getExtraInformation() { + return array(); + } + /** * Implements SearchApiServiceInterface::__construct(). * diff --git a/search_api.admin.css b/search_api.admin.css index 29d3b5e4..b82798df 100644 --- a/search_api.admin.css +++ b/search_api.admin.css @@ -15,10 +15,34 @@ vertical-align: top; } +/* + * VIEW SERVER + */ + +.search-api-server-summary ul.inline { + margin: 0; +} + +.search-api-server-summary ul.inline li { + padding-left: 0; +} + +/* + * VIEW INDEX + */ +.search-api-limit, +.search-api-batch-size { + text-align: center; +} + +.search-api-index-status .progress .filled { + background: #0074BD none; +} + /* * DROPBUTTONS * - * (Largely copied from D8's dropbutton.css. + * (Largely copied from D8's dropbutton.css.) */ /** diff --git a/search_api.admin.inc b/search_api.admin.inc index bc950e47..51071b12 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -80,7 +80,7 @@ function search_api_admin_overview() { } $links = array_merge($links, menu_contextual_links('search-api-server', $pre_server, array($server->machine_name))); $row[] = theme('search_api_dropbutton', array('links' => $links)); - $rows[] = $row; + $rows[] = _search_api_deep_copy($row); if (!empty($indexes[$server->machine_name])) { foreach ($indexes[$server->machine_name] as $index) { @@ -90,7 +90,7 @@ function search_api_admin_overview() { if ($show_config_status) { $row[] = theme('entity_status', array('status' => $index->status)); } - $row[] = ''; + $row[] = ' '; $row[] = $t_index; $row[] = l($index->name, $url); $links = array(); @@ -103,7 +103,7 @@ function search_api_admin_overview() { } $links = array_merge($links, menu_contextual_links('search-api-index', $pre_index, array($index->machine_name))); $row[] = theme('search_api_dropbutton', array('links' => $links)); - $rows[] = $row; + $rows[] = _search_api_deep_copy($row); } } } @@ -119,7 +119,7 @@ function search_api_admin_overview() { $row[] = l($index->name, $url); $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name)); $row[] = theme('search_api_dropbutton', array('links' => $links)); - $rows[] = $row; + $rows[] = _search_api_deep_copy($row); } } @@ -365,7 +365,7 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { } else { $ret = drupal_get_form('search_api_admin_confirm', 'server', $action, $server); - if ($ret) { + if (!empty($ret['actions'])) { return $ret; } } @@ -374,7 +374,18 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { drupal_set_title(search_api_admin_item_title($server)); $class = search_api_get_service_info($server->class); $options = $server->viewSettings(); - return array( + $indexes = array(); + foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) { + if (!$indexes) { + $indexes['#theme'] = 'links'; + $indexes['#attributes']['class'] = array('inline'); + } + $indexes['#links'][] = array( + 'title' => $index->name, + 'href' => 'admin/config/search/search_api/index/' . $index->machine_name, + ); + } + $render['view'] = array( '#theme' => 'search_api_server', '#id' => $server->id, '#name' => $server->name, @@ -383,9 +394,16 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { '#enabled' => $server->enabled, '#class_name' => $class['name'], '#class_description' => $class['description'], + '#indexes' => $indexes, '#options' => $options, '#status' => $server->status, + '#extra' => $server->getExtraInformation(), ); + $render['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; + if ($server->enabled) { + $render['form'] = drupal_get_form('search_api_server_status_form', $server); + } + return $render; } /** @@ -400,62 +418,140 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { * - enabled: Boolean indicating whether the server is enabled. * - class_name: The used service class' display name. * - class_description: The used service class' description. + * - indexes: A list of indexes associated with this server, either as an HTML + * string or a render array. * - options: An HTML string or render array containing information about the * server's service-specific settings. * - status: The entity configuration status (in database, in code, etc.). + * - extra: An associative array of additional server information, with the + * keys being the labels and the values being the information. + * + * @return string + * HTML for displaying a server. + * + * @ingroup themeable */ function theme_search_api_server(array $variables) { - extract($variables); + $machine_name = $variables['machine_name']; + $description = $variables['description']; + $enabled = $variables['enabled']; + $class_name = $variables['class_name']; + $indexes = $variables['indexes']; + $options = $variables['options']; + $status = $variables['status']; + $extra = $variables['extra']; + + // First, output the index description if there is one set. $output = ''; - $output .= '

    ' . check_plain($name) . '

    ' . "\n"; + if ($description) { + $output .= '

    ' . nl2br(check_plain($description)) . '

    '; + } - $output .= '
    ' . "\n"; + // Then, display a table summarizing the index's status. + $rows = array(); + // Create a row template with references so we don't have to deal with the + // complicated structure for each individual row. + $row = array( + 'data' => array( + array('header' => TRUE), + '', + ), + 'class' => array(''), + ); + $label = & $row['data'][0]['data']; + $info = & $row['data'][1]; + $class = & $row['class'][0]; - $output .= '
    ' . t('Status') . '
    ' . "\n"; - $output .= '
    '; if ($enabled) { - $output .= t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/server/' . $machine_name . '/disable'))); + $class = 'ok'; + $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/server/' . $machine_name . '/disable'))); } else { - $output .= t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/server/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name)))))); + $class = 'warning'; + $info = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/server/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name)))))); } - $output .= '
    ' . "\n"; + $label = t('Status'); + $rows[] = _search_api_deep_copy($row); + $class = ''; - $output .= '
    ' . t('Machine name') . '
    ' . "\n"; - $output .= '
    ' . check_plain($machine_name) . '
    ' . "\n"; + $label = t('Service class'); + if (module_exists('help')) { + $url_options['fragment'] = drupal_clean_css_identifier('search_api_solr_service'); + $info = l($class_name, 'admin/help/search_api', $url_options); + } + else { + $info = check_plain($class_name); + } + $rows[] = _search_api_deep_copy($row); - if (!empty($description)) { - $output .= '
    ' . t('Description') . '
    ' . "\n"; - $output .= '
    ' . nl2br(check_plain($description)) . '
    ' . "\n"; + if ($indexes) { + $label = t('Search indexes'); + $info = render($indexes); + $rows[] = _search_api_deep_copy($row); } - if (!empty($class_name)) { - $output .= '
    ' . t('Service class') . '
    ' . "\n"; - $output .= '
    ' . check_plain($class_name) . ''; - if (!empty($class_description)) { - $output .= '

    ' . $class_description . '

    '; - } - $output .= '
    ' . "\n"; + if ($options) { + $label = t('Service options'); + $info = render($options); + $rows[] = _search_api_deep_copy($row); } - if (!empty($options)) { - $output .= '
    ' . t('Service options') . '
    ' . "\n"; - $output .= '
    ' . "\n"; - $output .= render($options); - $output .= '
    ' . "\n"; + if ($status != ENTITY_CUSTOM) { + $label = t('Configuration status'); + $info = theme('entity_status', array('status' => $status)); + $class = ($status == ENTITY_OVERRIDDEN) ? 'warning' : 'ok'; + $rows[] = _search_api_deep_copy($row); + $class = ''; } - $output .= '
    ' . t('Configuration status') . '
    ' . "\n"; - $output .= '
    ' . "\n"; - $output .= theme('entity_status', array('status' => $status)); - $output .= '
    ' . "\n"; + if ($extra) { + foreach ($extra as $information) { + $label = $information['label']; + $info = $information['info']; + $class = !empty($information['status']) ? $information['status'] : ''; + $rows[] = _search_api_deep_copy($row); + } + } - $output .= '
    '; + $theme['rows'] = $rows; + $theme['attributes']['class'][] = 'search-api-summary'; + $theme['attributes']['class'][] = 'search-api-server-summary'; + $theme['attributes']['class'][] = 'system-status-report'; + $output .= theme('table', $theme); return $output; } +/** + * Form constructor for completely clearing a server. + * + * @param SearchApiServer $server + * The server for which the form is displayed. + * + * @ingroup forms + * + * @see search_api_server_status_form_submit() + */ +function search_api_server_status_form(array $form, array &$form_state, SearchApiServer $server) { + $form_state['server'] = $server; + + $form['clear'] = array( + '#type' => 'submit', + '#value' => t('Delete all indexed data on this server'), + ); + + return $form; +} + +/** +* Form submission handler for search_api_server_status_form(). +*/ +function search_api_server_status_form_submit(array $form, array &$form_state) { + $server_id = $form_state['server']->machine_name; + $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/clear"; +} + /** * Form constructor for editing a server's settings. * @@ -728,160 +824,286 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { } else { $ret = drupal_get_form('search_api_admin_confirm', 'index', $action, $index); - if ($ret) { + if (!empty($ret['actions'])) { return $ret; } } } - return drupal_get_form('search_api_admin_index_status_form', $index); + $status = search_api_index_status($index); + $ret['view'] = array( + '#theme' => 'search_api_index', + '#id' => $index->id, + '#name' => $index->name, + '#machine_name' => $index->machine_name, + '#description' => $index->description, + '#item_type' => $index->item_type, + '#enabled' => $index->enabled, + '#server' => $index->server(), + '#options' => $index->options, + '#fields' => $index->getFields(), + '#indexed_items' => $status['indexed'], + '#on_server' => _search_api_get_items_on_server($index), + '#total_items' => $status['total'], + '#status' => $index->status, + '#read_only' => $index->read_only, + ); + if ($index->enabled && !$index->read_only) { + $ret['form'] = drupal_get_form('search_api_admin_index_status_form', $index, $status); + } + return $ret; } /** - * Form function for displaying an index status form. + * Returns HTML for a search index. * - * @param SearchApiIndex $index - * The index whose status should be displayed. + * @param array $variables + * An associative array containing: + * - id: The index's id. + * - name: The index' name. + * - machine_name: The index' machine name. + * - description: The index' description. + * - item_type: The type of items stored in this index. + * - enabled: Boolean indicating whether the index is enabled. + * - server: The server this index currently rests on, if any. + * - options: The index' options, like cron limit. + * - fields: All indexed fields of the index. + * - indexed_items: The number of items already indexed in their latest + * version on this index. + * - on_server: The number of items actually indexed on the server. + * - total_items: The total number of items that have to be indexed for this + * index. + * - status: The entity configuration status (in database, in code, etc.). + * - read_only: Boolean indicating whether this index is read only. + * + * @return string + * HTML for a search index. + * + * @ingroup themeable */ -function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index) { - $enabled = !empty($index->enabled); - $server = $index->server(); - - $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; - $form_state['index'] = $index; +function theme_search_api_index(array $variables) { + $machine_name = $variables['machine_name']; + $description = $variables['description']; + $enabled = $variables['enabled']; + $item_type = $variables['item_type']; + $server = $variables['server']; + $options = $variables['options']; + $status = $variables['status']; + $indexed_items = $variables['indexed_items']; + $on_server = $variables['on_server']; + $total_items = $variables['total_items']; + + // First, output the index description if there is one set. + $output = ''; - if (!empty($index->description)) { - $form['description']['#markup'] = '

    ' . nl2br(check_plain($index->description)) . '

    '; + if ($description) { + $output .= '

    ' . nl2br(check_plain($description)) . '

    '; } - $form['info']['#prefix'] = '
    '; - $form['info']['#suffix'] = '
    '; + // Then, display a table summarizing the index's status. + $rows = array(); + // Create a row template with references so we don't have to deal with the + // complicated structure for each individual row. + $row = array( + 'data' => array( + array('header' => TRUE), + '', + ), + 'class' => array(''), + ); + $label = &$row['data'][0]['data']; + $info = &$row['data'][1]; + $class = &$row['class'][0]; + + $class = 'warning'; if ($enabled) { - $status_message = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $index->machine_name . '/disable'))); + $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $machine_name . '/disable'))); + $class = 'ok'; } - elseif (!empty($server->enabled)) { - $status_message = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/index/' . $index->machine_name . '/enable', array('query' => array('token' => drupal_get_token($index->machine_name)))))); + elseif ($server) { + $info = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/index/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name)))))); } else { - $status_message = t('disabled'); + $info = t('disabled'); } - $form['info']['status']['#markup'] = '
    ' . t('Status') . '
    ' . "\n"; - $form['info']['status']['#markup'] .= '
    ' . $status_message . '
    ' . "\n"; + $label = t('Status'); + $rows[] = _search_api_deep_copy($row); + $class = ''; - $type = search_api_get_item_type_info($index->item_type); - $form['info']['type']['#markup'] = '
    ' . t('Item type') . '
    ' . "\n"; - $form['info']['type']['#markup'] .= '
    ' . check_plain($type['name']) . '
    ' . "\n"; + $label = t('Item type'); + $type = search_api_get_item_type_info($item_type); + $item_type = !empty($type['name']) ? $type['name'] : $item_type; + $info = check_plain($item_type); + $rows[] = _search_api_deep_copy($row); - if (!empty($server)) { - $form['info']['server']['#markup'] = '
    ' . t('Server') . '
    ' . "\n"; - $form['info']['server']['#markup'] .= '
    ' . l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); - if (!empty($server->description)) { - $form['info']['server']['#markup'] .= '

    ' . nl2br(check_plain($server->description)) . '

    '; - } - $form['info']['server']['#markup'] .= '
    ' . "\n"; + if ($server) { + $label = t('Server'); + $info = l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); + $rows[] = _search_api_deep_copy($row); } - $form['info']['config_status']['#markup'] = '
    ' . t('Configuration status') . '
    ' . "\n"; - $form['info']['config_status']['#markup'] .= '
    ' . theme('entity_status', array('status' => $index->status)) . '
    ' . "\n"; + if ($enabled) { + $options += array('cron_limit' => SEARCH_API_DEFAULT_CRON_LIMIT); + if ($options['cron_limit']) { + $class = 'ok'; + $info = format_plural( + $options['cron_limit'], + 'During cron runs, 1 item will be indexed per batch.', + 'During cron runs, @count items will be indexed per batch.' + ); + } + else { + $class = 'warning'; + $info = t('No items will be indexed during cron runs.'); + } + $label = t('Cron batch size'); + $rows[] = _search_api_deep_copy($row); - if ($index->read_only) { - $message = t('The index is currently in read-only mode. No new items will be indexed, nor will old ones be deleted.'); - $form['info']['read_only']['#markup'] = '
    ' . t('Read only') . '
    ' . "\n"; - $form['info']['read_only']['#markup'] .= '
    ' . $message . '
    '; + $theme = array( + 'percent' => (int) (100 * $indexed_items / $total_items), + 'message' => t('@indexed/@total indexed', array('@indexed' => $indexed_items, '@total' => $total_items)), + ); + $output .= '

    ' . t('Index status') . '

    '; + $output .= '
    ' . theme('progress_bar', $theme) . '
    '; + + if ($on_server != $total_items) { + if ($on_server < $indexed_items || $on_server > $total_items) { + $vars = array(); + $vars['@num'] = $on_server; + if ($on_server < $indexed_items) { + $vars['@diff'] = $indexed_items - $on_server; + $info = t('The index status on the search server has diverged (@num items indexed – @diff less than required). You are strongly advised to mark the index for re-indexing using the form below.', $vars); + } + else { + $vars['@diff'] = $on_server - $total_items; + $info = t('The index status on the search server has diverged (@num items indexed – @diff more than present). You are strongly advised to clear the index using the form below.', $vars); + } + $class = 'error'; + } + else { + $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.'); + $class = 'warning'; + } + $label = t('Server index status'); + $rows[] = _search_api_deep_copy($row); + } + } - return $form; + if ($status != ENTITY_CUSTOM) { + $label = t('Configuration status'); + $info = theme('entity_status', array('status' => $status)); + $class = ($status == ENTITY_OVERRIDDEN) ? 'warning' : 'ok'; + $rows[] = _search_api_deep_copy($row); } - $status = search_api_index_status($index); + $theme['rows'] = $rows; + $theme['attributes']['class'][] = 'search-api-summary'; + $theme['attributes']['class'][] = 'search-api-index-summary'; + $theme['attributes']['class'][] = 'system-status-report'; + $output .= theme('table', $theme); + + return $output; +} + +/** + * Form constructor for an index status form. + * + * Should only be used for enabled indexes which aren't read-only. + * + * @param SearchApiIndex $index + * The index whose status should be displayed. + * @param array $status + * The indexing status of the index, as returned by search_api_index_status(). + * + * @ingroup forms + * + * @see search_api_admin_index_status_form_validate() + * @see search_api_admin_index_status_form_submit() + */ +function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index, array $status) { + $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; + $form_state['index'] = $index; + $form['index'] = array( '#type' => 'fieldset', - '#title' => t('Indexing status'), - '#description' => t('This index is disabled. No information about the indexing status is available.'), - '#collapsible' => TRUE, + '#title' => t('Index now'), ); - if ($enabled) { - $all = ($status['indexed'] == $status['total']); - if ($all) { - $form['index']['#description'] = t('All items have been indexed (@total / @total).', - array('@total' => $status['total'])); - } - elseif (!$status['indexed']) { - $form['index']['#description'] = t('All items still need to be indexed (@total total).', - array('@total' => $status['total'])); - } - else { - $percentage = (int) (100 * $status['indexed'] / $status['total']); - $form['index']['#description'] = t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', - array('@indexed' => $status['indexed'], '@total' => $status['total'], '@percentage' => $percentage)); - } + $form['index']['#attributes']['class'][] = 'container-inline'; - if (!$all) { - $form['index']['index'] = array( - '#type' => 'fieldset', - '#title' => t('Index now'), - '#collapsible' => TRUE, - ); - $form['index']['index']['settings'] = array( - '#type' => 'fieldset', - '#title' => t('Advanced settings'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form['index']['index']['settings']['limit'] = array( - '#type' => 'textfield', - '#title' => t('Number of items to index'), - '#default_value' => -1, - '#size' => 4, - '#attributes' => array('class' => array('search-api-limit')), - '#description' => t('Number of items to index. Set to -1 for all items.'), - ); - $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit']; - $form['index']['index']['settings']['batch_size'] = array( - '#type' => 'textfield', - '#title' => t('Number of items per batch run'), - '#default_value' => $batch_size, - '#size' => 4, - '#attributes' => array('class' => array('search-api-batch-size')), - '#description' => t('Number of items per batch run. Set to -1 for all items at once (not recommended). Defaults to the cron batch size of the index.'), - ); - $form['index']['index']['button'] = array( - '#type' => 'submit', - '#value' => t('Index now'), - ); - $form['index']['index']['total'] = array( - '#type' => 'value', - '#value' => $status['total'], - ); - $form['index']['index']['remaining'] = array( - '#type' => 'value', - '#value' => $status['total'] - $status['indexed'], - ); - } - } + $allow_indexing = ($status['indexed'] < $status['total']); + $all = t('all', array(), array('context' => 'items to index')); + $limit = array( + '#type' => 'textfield', + '#default_value' => $all, + '#size' => 4, + '#attributes' => array('class' => array('search-api-limit')), + '#disabled' => !$allow_indexing, + ); + $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit']; + $batch_size = $batch_size > 0 ? $batch_size : $all; + $batch_size = array( + '#type' => 'textfield', + '#default_value' => $batch_size, + '#size' => 4, + '#attributes' => array('class' => array('search-api-batch-size')), + '#disabled' => !$allow_indexing, + ); - if ($server) { - $form['index']['reindex'] = array( - '#type' => 'fieldset', - '#title' => t('Re-indexing'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#tree' => TRUE, - ); - $form['index']['reindex']['message'] = array( - '#type' => 'item', - '#description' => t('This will mark all items as "changed" and add them to the index again (overwriting existing data) in subsequent indexing operations.'), - ); - $form['index']['reindex']['clear'] = array( - '#type' => 'checkbox', - '#title' => t('Also clear data on server'), - '#description' => t('If checked, indexed data on the server will be deleted, too. No results will be returned by searches for this index until items are indexed again.
    Use with care, in most cases rebuilding the index might be enough.'), - '#default_value' => FALSE, - ); - $form['index']['reindex']['button'] = array( - '#type' => 'submit', - '#value' => t('Re-index content'), - ); + // Here it gets complicated. We want to build a sentence from the form input + // elements, but to translate that we have to make the two form elements (for + // limit and batch size) pseudo-variables in the t() call. Since we can't + // pass them directly, we split the translated sentence (which still has the + // two tokens), figure out their order and then put the pieces together again + // using the form elements' #prefix and #suffix properties. + $sentence = t('Index @limit items in batches of @batch_size items'); + $sentence = preg_split('/@(limit|batch_size)/', $sentence, -1, PREG_SPLIT_DELIM_CAPTURE); + if (count($sentence) == 5) { + $first = $sentence[1]; + $form['index'][$first] = $$first; + $form['index'][$first]['#prefix'] = $sentence[0]; + $form['index'][$first]['#suffix'] = $sentence[2]; + $second = $sentence[3]; + $form['index'][$second] = $$second; + $form['index'][$second]['#suffix'] = $sentence[4] . ' '; } + else { + // PANIC! + $limit['#title'] = t('Number of items to index'); + $form['index']['limit'] = $limit; + $batch_size['#title'] = t('Number of items per batch run'); + $form['index']['batch_size'] = $batch_size; + } + + $form['index']['button'] = array( + '#type' => 'submit', + '#value' => t('Index now'), + '#disabled' => !$allow_indexing, + ); + $form['index']['total'] = array( + '#type' => 'value', + '#value' => $status['total'], + ); + $form['index']['remaining'] = array( + '#type' => 'value', + '#value' => $status['total'] - $status['indexed'], + ); + $form['index']['all'] = array( + '#type' => 'value', + '#value' => $all, + ); + + $form['reindex'] = array( + '#type' => 'submit', + '#value' => t('Queue all items for reindexing'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $form['clear'] = array( + '#type' => 'submit', + '#value' => t('Clear all indexed data'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); return $form; } @@ -892,8 +1114,22 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea * @see search_api_admin_index_status_form_submit() */ function search_api_admin_index_status_form_validate(array $form, array &$form_state) { - if ($form_state['values']['op'] == t('Index now') && !$form_state['values']['limit']) { - form_set_error('number', t('You have to set the number of items to index. Set to -1 for indexing all items.')); + $values = $form_state['values']; + if ($values['op'] == t('Index now')) { + $all_lower = drupal_strtolower($values['all']); + foreach (array('limit', 'batch_size') as $field) { + $val = trim($values[$field]); + if (drupal_strtolower($val) == $all_lower) { + $val = -1; + } + elseif (!$val || !is_numeric($val) || ((int) $val) != $val) { + form_error($form['index'][$field], t('Enter a non-zero integer. Use "-1" or "@all" for "all items".', array('@all' => $values['all']))); + } + else { + $val = (int) $val; + } + $form_state['values'][$field] = $val; + } } } @@ -903,48 +1139,33 @@ function search_api_admin_index_status_form_validate(array $form, array &$form_s * @see search_api_admin_index_status_form_validate() */ function search_api_admin_index_status_form_submit(array $form, array &$form_state) { - $redirect = &$form_state['redirect']; $values = $form_state['values']; $index = $form_state['index']; - $pre = 'admin/config/search/search_api/index/' . $index->machine_name; + $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name; + + // There is a Form API bug here that will let a user submit the form via the + // "Index now" button even if it is disabled, and then just set "op" to the + // value of an arbitrary other button. We therefore have to take care to spot + // this case ourselves. + if ($form_state['input']['op'] == t('Index now') && !empty($form['index']['button']['#disabled'])) { + drupal_set_message(t('All items have already been indexed.'), 'warning'); + return; + } + switch ($values['op']) { - case t('Enable'): - $redirect = array( - $pre . '/enable', - array('query' => array('token' => drupal_get_token($index->machine_name))), - ); - break; - case t('Disable'): - $redirect = $pre . '/disable'; - break; case t('Index now'): if (!_search_api_batch_indexing_create($index, $values['batch_size'], $values['limit'], $values['remaining'])) { drupal_set_message(t("Couldn't create a batch, please check the batch size and limit."), 'warning'); } - $redirect = $pre; break; - case t('Re-index content'): - if (empty($values['reindex']['clear'])) { - if ($index->reindex()) { - drupal_set_message(t('The index was successfully scheduled for re-indexing.')); - } - else { - drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); - } - } - else { - if ($index->clear()) { - drupal_set_message(t('The index was successfully cleared.')); - } - else { - drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); - } - } - $redirect = $pre; + + case t('Queue all items for reindexing'): + $form_state['redirect'] .= '/reindex'; break; - default: - throw new SearchApiException(t('Unknown action.')); + case t('Clear all indexed data'): + $form_state['redirect'] .= '/clear'; + break; } } @@ -1760,7 +1981,7 @@ function theme_search_api_admin_fields_table($variables) { } } if (empty($form['fields'][$name]['description']['#value'])) { - $rows[] = $row; + $rows[] = _search_api_deep_copy($row); } else { $rows[] = array( @@ -1870,14 +2091,24 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio switch ($type) { case 'server': switch ($action) { - case 'disable': + case 'clear': $text = array( + t('Clear server @name', array('@name' => $entity->name)), + t('Do you really want to clear all indexed data from this server?'), + t('This will permanently remove all data currently indexed on this server. Before the data is reindexed, searches on the indexes associated with this server will not return any results. This action cannot be undone. Use with caution!'), + t("The server's indexed data was successfully cleared."), + ); + break; + + case 'disable': + array( t('Disable server @name', array('@name' => $entity->name)), t('Do you really want to disable this server?'), t('This will disconnect all indexes from this server and disable them. Searches on these indexes will not be available until they are added to another server and re-enabled. All indexed data (except for read-only indexes) on this server will be cleared.'), t('The server and its indexes were successfully disabled.'), ); break; + case 'delete': if ($entity->hasStatus(ENTITY_OVERRIDDEN)) { $text = array( @@ -1897,12 +2128,31 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio ); } break; + default: return FALSE; } break; case 'index': switch ($action) { + case 'reindex': + $text = array( + t('Re-index index @name', array('@name' => $entity->name)), + t('Do you really want to queue all items on this index for re-indexing?'), + t('This will mark all items for this index to be marked as needing to be indexed. Searches on this index will continue to yield results while the items are being re-indexed. This action cannot be undone.'), + t('The index was successfully marked for re-indexing.'), + ); + break; + + case 'clear': + $text = array( + t('Clear index @name', array('@name' => $entity->name)), + t('Do you really want to clear the indexed data of this index?'), + t('This will remove all data currently indexed for this index. Before the data is reindexed, searches on the index will not return any results. This action cannot be undone.'), + t('The index was successfully cleared.'), + ); + break; + case 'disable': $text = array( t('Disable index @name', array('@name' => $entity->name)), @@ -1911,6 +2161,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio t('The index was successfully disabled.'), ); break; + case 'delete': if ($entity->hasStatus(ENTITY_OVERRIDDEN)) { $text = array( @@ -1930,6 +2181,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio ); } break; + default: return FALSE; } diff --git a/search_api.api.php b/search_api.api.php index d4211262..c1af2a14 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -22,7 +22,8 @@ * - description: A translated string to be shown to administrators when * selecting a service class. Should contain all peculiarities of the * service class, like field type support, supported features (like facets), - * the "direct" parse mode and other specific things to keep in mind. + * the "direct" parse mode and other specific things to keep in mind. The + * text can contain HTML. * - class: The service class, which has to implement the * SearchApiServiceInterface interface. * diff --git a/search_api.module b/search_api.module index 431df9f9..1326abe8 100644 --- a/search_api.module +++ b/search_api.module @@ -59,7 +59,6 @@ function search_api_menu() { $items[$pre . '/server/%search_api_server/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, 'weight' => -10, ); $items[$pre . '/server/%search_api_server/edit'] = array( @@ -112,7 +111,6 @@ function search_api_menu() { $items[$pre . '/index/%search_api_index/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, 'weight' => -10, ); $items[$pre . '/index/%search_api_index/edit'] = array( @@ -183,6 +181,20 @@ function search_api_menu() { */ function search_api_help($path) { switch ($path) { + case 'admin/help#search_api': + $classes = array(); + foreach (search_api_get_service_info() as $id => $info) { + $id = drupal_clean_css_identifier($id); + $name = check_plain($info['name']); + $description = isset($info['description']) ? $info['description'] : ''; + $classes[] = "

    $name

    \n$description"; + } + $output = ''; + if ($classes) { + $output .= '

    ' . t('The following service classes are available for creating a search server.') . "

    \n"; + $output .= implode("\n\n", $classes); + } + return $output; case 'admin/config/search/search_api': return '

    ' . t('A search server and search index are used to execute searches. Several indexes can exist per server.
    You need at least one server and one index to create searches on your site.') . '

    '; } @@ -243,8 +255,29 @@ function search_api_theme() { 'enabled' => NULL, 'class_name' => NULL, 'class_description' => NULL, + 'indexes' => array(), + 'options' => array(), + 'status' => ENTITY_CUSTOM, + 'extra' => array(), + ), + 'file' => 'search_api.admin.inc', + ); + $themes['search_api_index'] = array( + 'variables' => array( + 'id' => NULL, + 'name' => '', + 'machine_name' => '', + 'description' => NULL, + 'item_type' => NULL, + 'enabled' => NULL, + 'server' => NULL, 'options' => array(), + 'fields' => array(), + 'indexed_items' => 0, + 'on_server' => 0, + 'total_items' => 0, 'status' => ENTITY_CUSTOM, + 'read_only' => 0, ), 'file' => 'search_api.admin.inc', ); @@ -2397,6 +2430,30 @@ function search_api_server_disable($id) { return $ret ? 1 : $ret; } +/** + * Clears a search server. + * + * Will delete all items stored on the server and mark all associated indexes + * for re-indexing. + * + * @param int|string $id + * The ID or machine name of the server to clear. + * + * @return bool + * TRUE on success, FALSE on failure. + */ +function search_api_server_clear($id) { + $server = search_api_server_load($id); + $success = TRUE; + foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) { + $success &= $index->reindex(); + } + if ($success) { + $server->deleteItems(); + } + return $success; +} + /** * Deletes a search server and disables all associated indexes. * @@ -2728,6 +2785,63 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ return $values; } +/** + * Determines the number of items indexed on a server for a certain index. + * + * Used as a helper function in search_api_admin_index_view(). + * + * @param SearchApiIndex $index + * The index + * + * @return int + * The number of items found on the server for this index, if the latter is + * enabled. 0 otherwise. + */ +function _search_api_get_items_on_server(SearchApiIndex $index) { + if (!$index->enabled) { + return 0; + } + // We want the raw count, without facets or other filters. Therefore we don't + // use the query's execute() method but pass it straight to the server for + // evaluation. Since this circumvents the normal preprocessing, which sets the + // fields (on which some service classes might even rely when there are no + // keywords), we set them manually here. + $query = $index->query() + ->fields(array()) + ->range(0, 0); + $response = $index->server()->search($query); + return $response['result count']; +} + +/** + * Returns a deep copy of the input array. + * + * The behavior of PHP regarding arrays with references pointing to it is rather + * weird. Therefore, we use this helper function in theme_search_api_index() to + * create safe copies of such arrays. + * + * @param array $array + * The array to copy. + * + * @return array + * A deep copy of the array. + */ +function _search_api_deep_copy(array $array) { + $copy = array(); + foreach ($array as $k => $v) { + if (is_array($v)) { + $copy[$k] = _search_api_deep_copy($v); + } + elseif (is_object($v)) { + $copy[$k] = clone $v; + } + elseif ($v) { + $copy[$k] = $v; + } + } + return $copy; +} + /** * Creates and sets a batch for indexing items. * diff --git a/search_api.test b/search_api.test index b7e0752e..12d3240d 100644 --- a/search_api.test +++ b/search_api.test @@ -314,7 +314,6 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertTitle('Search API test server | Drupal', 'Correct title when viewing server.'); $this->assertText('A server used for testing.', 'Description displayed.'); $this->assertText('search_api_test_service', 'Service name displayed.'); - $this->assertText('search_api_test_service description', 'Service description displayed.'); $this->assertText('search_api_test foo bar', 'Service options displayed.'); } @@ -453,29 +452,40 @@ class SearchApiWebTest extends DrupalWebTestCase { * @param bool $check_buttons * (optional) Whether to check for the correct presence/absence of buttons. * Defaults to TRUE. + * @param int|null $on_server + * (optional) The number of items actually on the server. Defaults to + * $indexed. */ - protected function checkIndexStatus($indexed = 0, $total = 10, $check_buttons = TRUE) { + protected function checkIndexStatus($indexed = 0, $total = 10, $check_buttons = TRUE, $on_server = NULL) { $url = "admin/config/search/search_api/index/{$this->index_id}"; if (strpos($this->url, $url) === FALSE) { $this->drupalGet($url); } - $all = ($indexed == $total); - $correct_status = 'Correct index status displayed.'; - if ($all) { - $this->assertText(t('All items have been indexed (@total / @total).', array('@total' => $total)), $correct_status); + + $index_status = t('@indexed/@total indexed', array('@indexed' => $indexed, '@total' => $total)); + $this->assertText($index_status, 'Correct index status displayed.'); + + if (!isset($on_server)) { + $on_server = $indexed; } - elseif (!$indexed) { - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => $total)), $correct_status); + if ($on_server == $total) { + $this->assertNoText(t('Server index status'), 'No server index status displayed.'); } else { - $percentage = (int) (100 * $indexed / $total); - $text = t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', - array( - '@indexed' => $indexed, - '@total' => $total, - '@percentage' => $percentage - )); - $this->assertText($text, $correct_status); + $vars['@num'] = $on_server; + if ($on_server < $indexed) { + $vars['@diff'] = $indexed - $on_server; + $info = t('The index status on the search server has diverged (@num items indexed – @diff less than required). You are strongly advised to mark the index for re-indexing using the form below.', $vars); + } + elseif ($on_server > $total) { + $vars['@diff'] = $on_server - $total; + $info = t('The index status on the search server has diverged (@num items indexed – @diff more than present). You are strongly advised to clear the index using the form below.', $vars); + } + else { + $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.'); + } + $this->assertText(t('Server index status'), 'Server index status displayed.'); + $this->assertText($info, 'Correct server index status displayed.'); } if (!$check_buttons) { @@ -483,11 +493,11 @@ class SearchApiWebTest extends DrupalWebTestCase { } $this->assertText(t('enabled'), '"Enabled" status displayed.'); - if ($all) { - $this->assertNoText(t('Index now'), '"Index now" form not displayed.'); + if ($indexed == $total) { + $this->assertRaw('disabled="disabled"', '"Index now" form disabled.'); } else { - $this->assertText(t('Index now'), '"Index now" form displayed.'); + $this->assertNoRaw('disabled="disabled"', '"Index now" form enabled.'); } } @@ -554,7 +564,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->insertItems(1); // item 14 // Check whether the status display is right. - $this->checkIndexStatus(7, 14, FALSE); + $this->checkIndexStatus(7, 14, FALSE, 0); // Indexing order should now be: 11, 12, 13, 14, 8, 2, 4. Let's try it out! // First manually index one item, and see if it's 11. @@ -565,7 +575,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertText(t('Successfully indexed @count item.', array('@count' => 1))); $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed."); $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed."); - $this->checkIndexStatus(8, 14, FALSE); + $this->checkIndexStatus(8, 14, FALSE, 1); $results = $this->doSearch(); $this->assertEqual($results['result count'], 1, 'Indexing order test 1: correct result count.'); @@ -696,9 +706,10 @@ class SearchApiWebTest extends DrupalWebTestCase { * Tests whether clearing the index works correctly. */ protected function clearIndex() { - $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}", array('reindex[clear]' => TRUE), t('Re-index content')); + $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}", array(), t('Clear all indexed data')); + $this->drupalPost(NULL, array(), t('Confirm')); $this->assertText(t('The index was successfully cleared.')); - $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 14)), 'Correct index status displayed.'); + $this->assertText(t('@indexed/@total indexed', array('@indexed' => 0, '@total' => 14)), 'Correct index status displayed.'); } /** From 4bc2ea2d63aa3b2307d8954f15707d832c3c562d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Dec 2013 09:56:39 +0100 Subject: [PATCH 070/278] Adapted CHANGELOG.txt to 1.10 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 23fa74cf..bbf0a3ef 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.10 (12/09/2013): +----------------------------- - #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs. - #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c. - #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too. From 8a96aae1bc5ae80ea3e3197678049be2ed1e0817 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Dec 2013 09:57:13 +0100 Subject: [PATCH 071/278] Added dev release to CHANGELOG.txt again. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bbf0a3ef..447cb83c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.10 (12/09/2013): ----------------------------- - #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs. From 38ac5e56a3410769ed288122a8f6013af203015a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 11 Dec 2013 11:23:11 +0100 Subject: [PATCH 072/278] Follow-up to #2130819 by drunken monkey: Fixed service class description link in server view. --- search_api.admin.inc | 9 ++++++--- search_api.module | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/search_api.admin.inc b/search_api.admin.inc index 51071b12..9d890a12 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -392,6 +392,7 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { '#machine_name' => $server->machine_name, '#description' => $server->description, '#enabled' => $server->enabled, + '#class_id' => $server->class, '#class_name' => $class['name'], '#class_description' => $class['description'], '#indexes' => $indexes, @@ -416,6 +417,7 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { * - machine_name: The server's machine name. * - description: The server's description. * - enabled: Boolean indicating whether the server is enabled. + * - class_id: The used service class' ID. * - class_name: The used service class' display name. * - class_description: The used service class' description. * - indexes: A list of indexes associated with this server, either as an HTML @@ -423,8 +425,8 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { * - options: An HTML string or render array containing information about the * server's service-specific settings. * - status: The entity configuration status (in database, in code, etc.). - * - extra: An associative array of additional server information, with the - * keys being the labels and the values being the information. + * - extra: An array of additional server information in the format specified + * by SearchApiAbstractService::getExtraInformation(). * * @return string * HTML for displaying a server. @@ -435,6 +437,7 @@ function theme_search_api_server(array $variables) { $machine_name = $variables['machine_name']; $description = $variables['description']; $enabled = $variables['enabled']; + $class_id = $variables['class_id']; $class_name = $variables['class_name']; $indexes = $variables['indexes']; $options = $variables['options']; @@ -477,7 +480,7 @@ function theme_search_api_server(array $variables) { $label = t('Service class'); if (module_exists('help')) { - $url_options['fragment'] = drupal_clean_css_identifier('search_api_solr_service'); + $url_options['fragment'] = drupal_clean_css_identifier($class_id); $info = l($class_name, 'admin/help/search_api', $url_options); } else { diff --git a/search_api.module b/search_api.module index 1326abe8..7c44dbe0 100644 --- a/search_api.module +++ b/search_api.module @@ -253,6 +253,7 @@ function search_api_theme() { 'machine_name' => '', 'description' => NULL, 'enabled' => NULL, + 'class_id' => NULL, 'class_name' => NULL, 'class_description' => NULL, 'indexes' => array(), From 0773327a0f169f69d00dfc85eee0cfea3b40dd14 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 13 Dec 2013 10:25:50 +0100 Subject: [PATCH 073/278] Issue #2150347 by drunken monkey: Added access callbacks for indexes and servers. --- CHANGELOG.txt | 1 + search_api.module | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 447cb83c..c73ea285 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2150347 by drunken monkey: Added access callbacks for indexes and servers. Search API 1.10 (12/09/2013): ----------------------------- diff --git a/search_api.module b/search_api.module index 7c44dbe0..4086764e 100644 --- a/search_api.module +++ b/search_api.module @@ -392,6 +392,7 @@ function search_api_entity_info() { 'entity class' => 'SearchApiServer', 'base table' => 'search_api_server', 'uri callback' => 'search_api_server_url', + 'access callback' => 'search_api_entity_access', 'module' => 'search_api', 'exportable' => TRUE, 'entity keys' => array( @@ -407,6 +408,7 @@ function search_api_entity_info() { 'entity class' => 'SearchApiIndex', 'base table' => 'search_api_index', 'uri callback' => 'search_api_index_url', + 'access callback' => 'search_api_entity_access', 'module' => 'search_api', 'exportable' => TRUE, 'entity keys' => array( @@ -2362,6 +2364,15 @@ function search_api_access_delete_page(Entity $entity) { return user_access('administer search_api') && $entity->hasStatus(ENTITY_CUSTOM); } +/** + * Determines whether a user can access a certain search server or index. + * + * Used as an access callback in search_api_entity_info(). + */ +function search_api_entity_access() { + return user_access('administer search_api'); +} + /** * Inserts a new search server into the database. * From e1d10260c8158d2292458a1aa2c7dbe6f0412910 Mon Sep 17 00:00:00 2001 From: timkang Date: Fri, 13 Dec 2013 11:52:28 +0100 Subject: [PATCH 074/278] Issue #2146435 by timkang: Fixed Views paging with custom pager add-ons. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c73ea285..5d1d63ee 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2146435 by timkang: Fixed Views paging with custom pager add-ons. - #2150347 by drunken monkey: Added access callbacks for indexes and servers. Search API 1.10 (12/09/2013): diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 2e7b2af5..06375e7a 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -248,16 +248,6 @@ class SearchApiViewsQuery extends views_plugin_query { $this->pager->query(); $this->query->setOption('skip result count', !$this->pager->use_count_query()); - // Views passes sometimes NULL and sometimes the integer 0 for "All" in a - // pager. If set to 0 items, a string "0" is passed. Therefore, we unset - // the limit if an empty value OTHER than a string "0" was passed. - if (!$this->limit && $this->limit !== '0') { - $this->limit = NULL; - } - // Set the range. (We always set this, as there might even be an offset if - // all items are shown.) - $this->query->range($this->offset, $this->limit); - // Set the search ID, if it was not already set. if ($this->query->getOption('search id') == get_class($this->query)) { $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display); @@ -307,6 +297,16 @@ class SearchApiViewsQuery extends views_plugin_query { // Trigger pager pre_execute(). $this->pager->pre_execute($this->query); + // Views passes sometimes NULL and sometimes the integer 0 for "All" in a + // pager. If set to 0 items, a string "0" is passed. Therefore, we unset + // the limit if an empty value OTHER than a string "0" was passed. + if (!$this->limit && $this->limit !== '0') { + $this->limit = NULL; + } + // Set the range. (We always set this, as there might even be an offset if + // all items are shown.) + $this->query->range($this->offset, $this->limit); + $start = microtime(TRUE); // Execute the search. From 201bd982dcd703c53ba6c2a24a0b0b29cf08354c Mon Sep 17 00:00:00 2001 From: "pool.fi" Date: Fri, 13 Dec 2013 15:24:38 +0100 Subject: [PATCH 075/278] Issue #2156021 by jgullstr: Fixed confirm message when disabling servers. --- CHANGELOG.txt | 1 + search_api.admin.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d1d63ee..6c016b20 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2156021 by jgullstr: Fixed confirm message when disabling servers. - #2146435 by timkang: Fixed Views paging with custom pager add-ons. - #2150347 by drunken monkey: Added access callbacks for indexes and servers. diff --git a/search_api.admin.inc b/search_api.admin.inc index 9d890a12..6854f644 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -2104,7 +2104,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio break; case 'disable': - array( + $text = array( t('Disable server @name', array('@name' => $entity->name)), t('Do you really want to disable this server?'), t('This will disconnect all indexes from this server and disable them. Searches on these indexes will not be available until they are added to another server and re-enabled. All indexed data (except for read-only indexes) on this server will be cleared.'), From c50aad5e6b9739cb9ef427996c5f50fc1b56a4b0 Mon Sep 17 00:00:00 2001 From: drumm Date: Wed, 18 Dec 2013 08:34:41 +0100 Subject: [PATCH 076/278] Issue #2158873 by drumm, drunken monkey: Fixed "all of" operator of Views entity filter handler. --- CHANGELOG.txt | 2 ++ .../search_api_views/includes/handler_filter_entity.inc | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6c016b20..e5cc963f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2158873 by drumm, drunken monkey: Fixed "all of" operator of Views entity + filter handler. - #2156021 by jgullstr: Fixed confirm message when disabling servers. - #2146435 by timkang: Fixed Views paging with custom pager add-ons. - #2150347 by drunken monkey: Added access callbacks for indexes and servers. diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index 1dbdb29a..7fb2e240 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -193,13 +193,15 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi $this->query->condition($this->real_field, NULL, '<>', $this->options['group']); } elseif (is_array($this->value)) { + $all_of = $this->operator === 'all of'; + $operator = $all_of ? '=' : $this->operator; if (count($this->value) == 1) { - $this->query->condition($this->real_field, reset($this->value), $this->operator, $this->options['group']); + $this->query->condition($this->real_field, reset($this->value), $operator, $this->options['group']); } else { - $filter = $this->query->createFilter($this->operator === '<>' || $this->operator === 'all of' ? 'AND' : 'OR'); + $filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR'); foreach ($this->value as $value) { - $filter->condition($this->real_field, $value, $this->operator === 'all of' ? '=' : $this->operator); + $filter->condition($this->real_field, $value, $operator); } $this->query->filter($filter, $this->options['group']); } From 10fc54c8f80ba056da7087e2ea78861a4aa88aec Mon Sep 17 00:00:00 2001 From: rjacobs Date: Tue, 24 Dec 2013 08:31:53 +0100 Subject: [PATCH 077/278] Issue #2155721 by rjacobs, drunken monkey: Added support for Views' get_total_rows property. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e5cc963f..9e078d58 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2155721 by rjacobs, drunken monkey: Added support for Views' get_total_rows + property. - #2158873 by drumm, drunken monkey: Fixed "all of" operator of Views entity filter handler. - #2156021 by jgullstr: Fixed confirm message when disabling servers. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 06375e7a..36bc2326 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -246,7 +246,6 @@ class SearchApiViewsQuery extends views_plugin_query { // Initialize the pager and let it modify the query to add limits. $view->init_pager(); $this->pager->query(); - $this->query->setOption('skip result count', !$this->pager->use_count_query()); // Set the search ID, if it was not already set. if ($this->query->getOption('search id') == get_class($this->query)) { @@ -293,6 +292,14 @@ class SearchApiViewsQuery extends views_plugin_query { return; } + // Calculate the "skip result count" option, if it wasn't already set to + // FALSE. + $skip_result_count = $this->query->getOption('skip result count', TRUE); + if ($skip_result_count) { + $skip_result_count = !$this->pager->use_count_query() && empty($view->get_total_rows); + $this->query->setOption('skip result count', $skip_result_count); + } + try { // Trigger pager pre_execute(). $this->pager->pre_execute($this->query); @@ -314,7 +321,7 @@ class SearchApiViewsQuery extends views_plugin_query { $this->search_api_results = $results; // Store the results. - if ($this->pager->use_count_query()) { + if (!$skip_result_count) { $this->pager->total_items = $view->total_rows = $results['result count']; if (!empty($this->pager->options['offset'])) { $this->pager->total_items -= $this->pager->options['offset']; From 4ab448a8fff1fb8e2d4e125a4d9e690988d72a3c Mon Sep 17 00:00:00 2001 From: idebr Date: Tue, 24 Dec 2013 18:37:29 +0100 Subject: [PATCH 078/278] Issue #2159011 by idebr, drunken monkey: Fixed highlighting of keywords with PCRE special characters. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9e078d58..8feefc13 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2159011 by idebr, drunken monkey: Fixed highlighting of keywords with PCRE + special characters. - #2155721 by rjacobs, drunken monkey: Added support for Views' get_total_rows property. - #2158873 by drumm, drunken monkey: Fixed "all of" operator of Views entity diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 2c7f3676..eda797c8 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -395,7 +395,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { */ protected function highlightField($text, array $keys) { $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; - $text = preg_replace('/' . self::$boundary . '(' . implode('|', $keys) . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); + $keys = implode('|', array_map('preg_quote', $keys)); + $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); return substr($text, 1, -1); } From 821537ddd59dd4ded715583dee00526bd1904cdf Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 24 Dec 2013 19:06:20 +0100 Subject: [PATCH 079/278] Follow-up to #2118589 by drunken monkey: Fixed false error messages when "Node access" data alteration is disabled. --- search_api.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_api.module b/search_api.module index 4086764e..35ee56f7 100644 --- a/search_api.module +++ b/search_api.module @@ -1921,7 +1921,7 @@ function search_api_search_api_query_alter(SearchApiQueryInterface $query) { // Only add node access if the necessary fields are indexed in the index, and // unless disabled explicitly by the query. $type = $index->getEntityType(); - if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]) && !$query->getOption('search_api_bypass_access')) { + if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status']) && !$query->getOption('search_api_bypass_access')) { $account = $query->getOption('search_api_access_account', $user); if (is_numeric($account)) { $account = user_load($account); From 120a45c2148c0a0bbac22d0dfb55c9909b14f7ff Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 10:26:24 +0100 Subject: [PATCH 080/278] Issue #2155575 by drunken monkey: Fixed incorrect "Server index status" warnings. --- CHANGELOG.txt | 1 + search_api.admin.inc | 26 +++++--------------------- search_api.test | 22 +++------------------- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8feefc13..1f9f6da6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2155575 by drunken monkey: Fixed incorrect "Server index status" warnings. - #2159011 by idebr, drunken monkey: Fixed highlighting of keywords with PCRE special characters. - #2155721 by rjacobs, drunken monkey: Added support for Views' get_total_rows diff --git a/search_api.admin.inc b/search_api.admin.inc index 6854f644..f4210f4f 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -969,27 +969,11 @@ function theme_search_api_index(array $variables) { $output .= '

    ' . t('Index status') . '

    '; $output .= '
    ' . theme('progress_bar', $theme) . '
    '; - if ($on_server != $total_items) { - if ($on_server < $indexed_items || $on_server > $total_items) { - $vars = array(); - $vars['@num'] = $on_server; - if ($on_server < $indexed_items) { - $vars['@diff'] = $indexed_items - $on_server; - $info = t('The index status on the search server has diverged (@num items indexed – @diff less than required). You are strongly advised to mark the index for re-indexing using the form below.', $vars); - } - else { - $vars['@diff'] = $on_server - $total_items; - $info = t('The index status on the search server has diverged (@num items indexed – @diff more than present). You are strongly advised to clear the index using the form below.', $vars); - } - $class = 'error'; - } - else { - $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.'); - $class = 'warning'; - } - $label = t('Server index status'); - $rows[] = _search_api_deep_copy($row); - } + $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status'); + $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (More information)', 'There are @count items indexed on the server for this index. (More information)', $vars); + $class = ''; + $label = t('Server index status'); + $rows[] = _search_api_deep_copy($row); } if ($status != ENTITY_CUSTOM) { diff --git a/search_api.test b/search_api.test index 12d3240d..47479812 100644 --- a/search_api.test +++ b/search_api.test @@ -468,25 +468,9 @@ class SearchApiWebTest extends DrupalWebTestCase { if (!isset($on_server)) { $on_server = $indexed; } - if ($on_server == $total) { - $this->assertNoText(t('Server index status'), 'No server index status displayed.'); - } - else { - $vars['@num'] = $on_server; - if ($on_server < $indexed) { - $vars['@diff'] = $indexed - $on_server; - $info = t('The index status on the search server has diverged (@num items indexed – @diff less than required). You are strongly advised to mark the index for re-indexing using the form below.', $vars); - } - elseif ($on_server > $total) { - $vars['@diff'] = $on_server - $total; - $info = t('The index status on the search server has diverged (@num items indexed – @diff more than present). You are strongly advised to clear the index using the form below.', $vars); - } - else { - $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.'); - } - $this->assertText(t('Server index status'), 'Server index status displayed.'); - $this->assertText($info, 'Correct server index status displayed.'); - } + $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.'); + $this->assertText(t('Server index status'), 'Server index status displayed.'); + $this->assertText($info, 'Correct server index status displayed.'); if (!$check_buttons) { return; From 37a848bb108d6b8540f0ae03ce56febaf892e959 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 10:27:53 +0100 Subject: [PATCH 081/278] Issue #2155127 by drunken monkey: Clarified the scope of the "Node access" and "Exclude unpublished nodes" data alterations. --- CHANGELOG.txt | 2 ++ search_api.module | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1f9f6da6..142740a9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2155127 by drunken monkey: Clarified the scope of the "Node access" and + "Exclude unpublished nodes" data alterations. - #2155575 by drunken monkey: Fixed incorrect "Server index status" warnings. - #2159011 by idebr, drunken monkey: Fixed highlighting of keywords with PCRE special characters. diff --git a/search_api.module b/search_api.module index 35ee56f7..000cadb2 100644 --- a/search_api.module +++ b/search_api.module @@ -1012,17 +1012,17 @@ function search_api_search_api_alter_callback_info() { ); $callbacks['search_api_alter_node_access'] = array( 'name' => t('Node access'), - 'description' => t('Add node access information to the index.'), + 'description' => t('Add node access information to the index. Caution: This only affects the indexed nodes themselves, not any node reference fields that are indexed with them, or displayed in search results.'), 'class' => 'SearchApiAlterNodeAccess', ); $callbacks['search_api_alter_comment_access'] = array( 'name' => t('Access check'), - 'description' => t('Add node access information to the index.'), + 'description' => t('Add node access information to the index. Caution: This only affects the indexed nodes themselves, not any node reference fields that are indexed with them, or displayed in search results.'), 'class' => 'SearchApiAlterCommentAccess', ); $callbacks['search_api_alter_node_status'] = array( 'name' => t('Exclude unpublished nodes'), - 'description' => t('Exclude unpublished nodes from the index.'), + 'description' => t('Exclude unpublished nodes from the index. Caution: This only affects the indexed nodes themselves. If an enabled node has references to disabled nodes, those will still be indexed (or displayed) normally.'), 'class' => 'SearchApiAlterNodeStatus', ); From 4bf28b812b916ff5884e6d6604b88ef43bc06c09 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 10:33:43 +0100 Subject: [PATCH 082/278] Issue #1879196 by drunken monkey: Fixed invalid old indexes causing errors. --- CHANGELOG.txt | 1 + search_api.install | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 142740a9..a0dcebfd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1879196 by drunken monkey: Fixed invalid old indexes causing errors. - #2155127 by drunken monkey: Clarified the scope of the "Node access" and "Exclude unpublished nodes" data alterations. - #2155575 by drunken monkey: Fixed incorrect "Server index status" warnings. diff --git a/search_api.install b/search_api.install index 1d3b2043..86812a4c 100644 --- a/search_api.install +++ b/search_api.install @@ -966,3 +966,29 @@ function search_api_update_7116() { $insert->execute(); } } + +/** + * Checks the database for illegal {search_api_index}.server values. + */ +function search_api_update_7117() { + $servers = db_select('search_api_server', 's') + ->fields('s', array('machine_name')) + ->condition('enabled', 1); + $indexes = db_select('search_api_index', 'i') + ->fields('i', array('id')) + ->condition('server', $servers, 'NOT IN') + ->execute() + ->fetchCol(); + if ($indexes) { + db_delete('search_api_item') + ->condition('index_id', $indexes) + ->execute(); + db_update('search_api_index') + ->fields(array( + 'server' => NULL, + 'enabled' => 0, + )) + ->condition('id', $indexes) + ->execute(); + } +} From 9d3ad5f95008c619b911c60124fa050f81113e9e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 10:38:49 +0100 Subject: [PATCH 083/278] Adapted CHANGELOG.txt to 1.11 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a0dcebfd..605484d8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.11 (12/25/2013): +----------------------------- - #1879196 by drunken monkey: Fixed invalid old indexes causing errors. - #2155127 by drunken monkey: Clarified the scope of the "Node access" and "Exclude unpublished nodes" data alterations. From c4114defe6261bdf210402e05baf6fe3abb1ae25 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 10:39:11 +0100 Subject: [PATCH 084/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 605484d8..43189fbd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.11 (12/25/2013): ----------------------------- - #1879196 by drunken monkey: Fixed invalid old indexes causing errors. From 08a16821d8523bf24a05510568bbb98ee5b9eded Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 12:33:04 +0100 Subject: [PATCH 085/278] Issue #1227702 by drunken monkey: Improved error handling. --- CHANGELOG.txt | 1 + .../plugins/facetapi/query_type_term.inc | 11 +- .../search_api_facetapi.module | 28 +++- .../handler_argument_more_like_this.inc | 38 +++-- includes/index_entity.inc | 106 +++++++++---- search_api.admin.inc | 57 +++++-- search_api.drush.inc | 56 +++++-- search_api.install | 9 +- search_api.module | 145 +++++++++++------- search_api.rules.inc | 4 + 10 files changed, 328 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 43189fbd..3388b363 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1227702 by drunken monkey: Improved error handling. Search API 1.11 (12/25/2013): ----------------------------- diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 587598a3..bb91c179 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -30,7 +30,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // Return terms for this facet. $this->adapter->addFacet($this->facet, $query); - $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings; + $settings = $this->getSettings()->settings; // First check if the facet is enabled for this search. $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE; @@ -56,7 +56,12 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy $conjunction = 'OR'; } else { - throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator))); + $vars = array( + '%operator' => $operator, + '%facet' => !empty($this->facet['label']) ? $this->facet['label'] : $this->facet['name'], + ); + watchdog('search_api_facetapi', 'Unknown facet operator %operator used for facet %facet.', $vars, WATCHDOG_WARNING); + return; } $tags = array('facet:' . $this->facet['field']); $facet_filter = $query->createFilter($conjunction, $tags); @@ -77,7 +82,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // Test if this filter should be negated. $settings = $this->adapter->getFacet($this->facet)->getSettings(); $exclude = !empty($settings->settings['exclude']); - // Integer (or other nun-string) filters might mess up some of the following + // Integer (or other non-string) filters might mess up some of the following // comparison expressions. $filter = (string) $filter; if ($filter == '!') { diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index 29a2dbd5..0e3da91c 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -53,7 +53,7 @@ function search_api_facetapi_facetapi_searcher_info() { $info = array(); $indexes = search_api_index_load_multiple(FALSE); foreach ($indexes as $index) { - if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) { + if (_search_api_facetapi_index_support_feature($index)) { $searcher_name = 'search_api@' . $index->machine_name; $info[$searcher_name] = array( 'label' => t('Search service: @name', array('@name' => $index->name)), @@ -116,7 +116,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) { 'description' => t('Filter by @type.', array('@type' => $field['name'])), 'allowed operators' => array( FACETAPI_OPERATOR_AND => TRUE, - FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'), + FACETAPI_OPERATOR_OR => _search_api_facetapi_index_support_feature($index, 'search_api_facets_operator_or'), ), 'dependency plugins' => array('role'), 'facet missing allowed' => TRUE, @@ -218,7 +218,7 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) { if (!$index->enabled) { return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.')); } - if (!$index->server()->supportsFeature('search_api_facets')) { + if (!_search_api_facetapi_index_support_feature($index)) { return array('#markup' => t('This index uses a server that does not support facet functionality.')); } $searcher_name = 'search_api@' . $index->machine_name; @@ -226,6 +226,28 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) { return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name); } +/** + * Checks whether a certain feature is supported for an index. + * + * @param SearchApiIndex $index + * The search index which should be checked. + * @param string $feature + * (optional) The feature to check for. Defaults to "search_api_facets". + * + * @return bool + * TRUE if the feature is supported by the index's server (and the index is + * currently enabled), FALSE otherwise. + */ +function _search_api_facetapi_index_support_feature(SearchApiIndex $index, $feature = 'search_api_facets') { + try { + $server = $index->server(); + return $server && $server->supportsFeature($feature); + } + catch (SearchApiException $e) { + return FALSE; + } +} + /** * Gets hierarchy information for taxonomy terms. * diff --git a/contrib/search_api_views/includes/handler_argument_more_like_this.inc b/contrib/search_api_views/includes/handler_argument_more_like_this.inc index 9683c253..69e4a54c 100644 --- a/contrib/search_api_views/includes/handler_argument_more_like_this.inc +++ b/contrib/search_api_views/includes/handler_argument_more_like_this.inc @@ -62,24 +62,30 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg * The argument sent may be found at $this->argument. */ public function query($group_by = FALSE) { - $server = $this->query->getIndex()->server(); - if (!$server->supportsFeature('search_api_mlt')) { - $class = search_api_get_service_info($server->class); - watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.', + try { + $server = $this->query->getIndex()->server(); + if (!$server->supportsFeature('search_api_mlt')) { + $class = search_api_get_service_info($server->class); + watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.', array('@class' => $class['name']), WATCHDOG_ERROR); - $this->query->abort(); - return; - } - $fields = $this->options['fields'] ? $this->options['fields'] : array(); - if (empty($fields)) { - foreach ($this->query->getIndex()->options['fields'] as $key => $field) { - $fields[] = $key; + $this->query->abort(); + return; + } + $fields = $this->options['fields'] ? $this->options['fields'] : array(); + if (empty($fields)) { + foreach ($this->query->getIndex()->options['fields'] as $key => $field) { + $fields[] = $key; + } } + $mlt = array( + 'id' => $this->argument, + 'fields' => $fields, + ); + $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt); + } + catch (SearchApiException $e) { + $this->query->abort($e->getMessage()); } - $mlt = array( - 'id' => $this->argument, - 'fields' => $fields, - ); - $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt); } + } diff --git a/includes/index_entity.inc b/includes/index_entity.inc index b2168842..10d5c901 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -180,12 +180,17 @@ class SearchApiIndex extends Entity { * Execute necessary tasks for a newly created index. */ public function postCreate() { - if ($this->enabled) { - $this->queueItems(); + try { + if ($server = $this->server()) { + // Tell the server about the new index. + $server->addIndex($this); + if ($this->enabled) { + $this->queueItems(); + } + } } - if ($server = $this->server()) { - // Tell the server about the new index. - $server->addIndex($this); + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); } } @@ -193,8 +198,13 @@ class SearchApiIndex extends Entity { * Execute necessary tasks when the index is removed from the database. */ public function postDelete() { - if ($server = $this->server()) { - $server->removeIndex($this); + try { + if ($server = $this->server()) { + $server->removeIndex($this); + } + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); } // Stop tracking entities for indexing. @@ -206,7 +216,12 @@ class SearchApiIndex extends Entity { */ public function queueItems() { if (!$this->read_only) { - $this->datasource()->startTracking(array($this)); + try { + $this->datasource()->startTracking(array($this)); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } } @@ -214,7 +229,12 @@ class SearchApiIndex extends Entity { * Remove all records of entities to index. */ public function dequeueItems() { - $this->datasource()->stopTracking(array($this)); + try { + $this->datasource()->stopTracking(array($this)); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } /** @@ -231,15 +251,20 @@ class SearchApiIndex extends Entity { if (empty($this->description)) { $this->description = NULL; } - if (empty($this->server)) { - $this->server = NULL; - $this->enabled = FALSE; + $server = FALSE; + if (!empty($this->server)) { + $server = search_api_server_load($this->server); + if (!$server) { + $vars['%server'] = $this->server; + $vars['%index'] = $this->name; + watchdog('search_api', 'Unknown server %server specified for index %index.', $vars, WATCHDOG_ERROR); + } } - // This will also throw an exception if the server doesn't exist – which is good. - elseif (!$this->server(TRUE)->enabled) { - $this->enabled = FALSE; + if (!$server) { $this->server = NULL; + $this->enabled = FALSE; } + $this->resetCaches(); return parent::save(); } @@ -305,7 +330,12 @@ class SearchApiIndex extends Entity { return TRUE; } - $this->server()->deleteItems('all', $this); + try { + $this->server()->deleteItems('all', $this); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } _search_api_index_reindex($this); module_invoke_all('search_api_index_reindex', $this, TRUE); @@ -350,7 +380,12 @@ class SearchApiIndex extends Entity { * otherwise. */ public function getEntityType() { - return $this->datasource()->getEntityType(); + try { + return $this->datasource()->getEntityType(); + } + catch (SearchApiException $e) { + return ''; + } } /** @@ -385,7 +420,7 @@ class SearchApiIndex extends Entity { * SearchApiQueryInterface::__construct(). * * @throws SearchApiException - * If the index is currently disabled. + * If the index is currently disabled or its server doesn't exist. * * @return SearchApiQueryInterface * A query object for searching this index. @@ -399,15 +434,20 @@ class SearchApiIndex extends Entity { /** - * Indexes items on this index. Will return an array of IDs of items that - * should be marked as indexed – i.e., items that were either rejected by a - * data-alter callback or were successfully indexed. + * Indexes items on this index. + * + * Will return an array of IDs of items that should be marked as indexed – + * i.e., items that were either rejected by a data-alter callback or were + * successfully indexed. * * @param array $items - * An array of items to index. + * An array of items to index, of this index's item type. * * @return array * An array of the IDs of all items that should be marked as indexed. + * + * @throws SearchApiException + * If an error occurred during indexing. */ public function index(array $items) { if ($this->read_only) { @@ -925,12 +965,18 @@ class SearchApiIndex extends Entity { * @return EntityMetadataWrapper * A wrapper for the item type of this index, optionally loaded with the * given data and having additional fields according to the data alterations - * of this index. + * of this index (if $alter wasn't set to FALSE). */ public function entityWrapper($item = NULL, $alter = TRUE) { - $info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties'; - $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties'; - return $this->datasource()->getMetadataWrapper($item, $info); + try { + $info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties'; + $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties'; + return $this->datasource()->getMetadataWrapper($item, $info); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + return entity_metadata_wrapper($this->item_type); + } } /** @@ -945,7 +991,13 @@ class SearchApiIndex extends Entity { * @see SearchApiDataSourceControllerInterface::loadItems() */ public function loadItems(array $ids) { - return $this->datasource()->loadItems($ids); + try { + return $this->datasource()->loadItems($ids); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + return array(); + } } /** diff --git a/search_api.admin.inc b/search_api.admin.inc index f4210f4f..a145ec7e 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -834,6 +834,15 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { } $status = search_api_index_status($index); + try { + $server = $index->server(); + } + catch (SearchApiException $e) { + $server = NULL; + $vars['%server'] = $index->server; + $message = t('The index has an unknown server (ID: %server) set. Please check the index settings.', $vars); + drupal_set_message($message, 'error'); + } $ret['view'] = array( '#theme' => 'search_api_index', '#id' => $index->id, @@ -842,15 +851,21 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { '#description' => $index->description, '#item_type' => $index->item_type, '#enabled' => $index->enabled, - '#server' => $index->server(), + '#server' => $server, '#options' => $index->options, '#fields' => $index->getFields(), '#indexed_items' => $status['indexed'], - '#on_server' => _search_api_get_items_on_server($index), + '#on_server' => NULL, '#total_items' => $status['total'], '#status' => $index->status, '#read_only' => $index->read_only, ); + try{ + $ret['view']['#on_server'] = _search_api_get_items_on_server($index); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } if ($index->enabled && !$index->read_only) { $ret['form'] = drupal_get_form('search_api_admin_index_status_form', $index, $status); } @@ -873,7 +888,8 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { * - fields: All indexed fields of the index. * - indexed_items: The number of items already indexed in their latest * version on this index. - * - on_server: The number of items actually indexed on the server. + * - on_server: The number of items actually indexed on the server. NULL if + * the search for finding out the item count failed. * - total_items: The total number of items that have to be indexed for this * index. * - status: The entity configuration status (in database, in code, etc.). @@ -963,15 +979,21 @@ function theme_search_api_index(array $variables) { $rows[] = _search_api_deep_copy($row); $theme = array( - 'percent' => (int) (100 * $indexed_items / $total_items), + 'percent' => $total_items ? (int) (100 * $indexed_items / $total_items) : 100, 'message' => t('@indexed/@total indexed', array('@indexed' => $indexed_items, '@total' => $total_items)), ); $output .= '

    ' . t('Index status') . '

    '; $output .= '
    ' . theme('progress_bar', $theme) . '
    '; - $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status'); - $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (More information)', 'There are @count items indexed on the server for this index. (More information)', $vars); - $class = ''; + if (!isset($on_server)) { + $info = t('An error occurred while trying to determine the server index status. Please check the logs for details.'); + $class = 'error'; + } + else { + $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status'); + $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (More information)', 'There are @count items indexed on the server for this index. (More information)', $vars); + $class = ''; + } $label = t('Server index status'); $rows[] = _search_api_deep_copy($row); } @@ -1178,12 +1200,21 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#default_value' => $index->name, '#required' => TRUE, ); + try { + $enabled_fixed = !$index->enabled && !$index->server(); + } + catch (Exception $e) { + watchdog_exception('search_api', $e); + // The exception only occurs if the index is disabled, and for an unknown + // server we of course want do prevent the index from being enabled. + $enabled_fixed = TRUE; + } $form['enabled'] = array( '#type' => 'checkbox', '#title' => t('Enabled'), '#default_value' => $index->enabled, // Can't enable an index lying on a disabled server, or no server at all. - '#disabled' => !$index->enabled && (!$index->server() || !$index->server()->enabled), + '#disabled' => $enabled_fixed, ); $form['description'] = array( '#type' => 'textarea', @@ -2209,8 +2240,16 @@ function search_api_admin_confirm_submit(array $form, array &$form_state) { $action = $values['action']; $id = $values['id']; + $success = FALSE; $function = "search_api_{$type}_{$action}"; - if ($function($id)) { + try { + // Some actions, like disabling, can actually throw an exception. + $success = $function($id); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } + if ($success) { drupal_set_message($values['message']); } else { diff --git a/search_api.drush.inc b/search_api.drush.inc index e1841e8f..d13dce1e 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -137,15 +137,21 @@ function drush_search_api_list() { foreach ($indexes as $index) { $type = search_api_get_item_type_info($index->item_type); $type = isset($type['name']) ? $type['name'] : $index->item_type; - $server = $index->server(); - $server = $server ? $server->name : '(' . t('none') . ')'; + try { + $server = $index->server(); + $server = $server ? $server->name : '(' . dt('none') . ')'; + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + $server = '(' . dt('unknown: !server', array('server' => $index->server)) . ')'; + } $row = array( $index->id, $index->name, $index->machine_name, $server, $type, - $index->enabled ? t('enabled') : t('disabled'), + $index->enabled ? dt('enabled') : dt('disabled'), $index->options['cron_limit'], ); $rows[] = $row; @@ -168,17 +174,24 @@ function drush_search_api_enable($index_id = NULL) { return; } foreach ($indexes as $index) { + $vars = array('!index' => $index->name); if (!$index->enabled) { - drush_log(dt("Enabling index !index and queueing items for indexing.", array('!index' => $index->name)), 'notice'); - if (search_api_index_enable($index->id)) { - drush_log(dt("The index !index was successfully enabled.", array('!index' => $index->name)), 'ok'); + drush_log(dt("Enabling index !index and queueing items for indexing.", $vars), 'notice'); + $success = FALSE; + try { + if ($success = search_api_index_enable($index->id)) { + drush_log(dt("The index !index was successfully enabled.", $vars), 'ok'); + } } - else { - drush_log(dt("Error enabling index !index.", array('!index' => $index->name)), 'error'); + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); + } + if (!$success) { + drush_log(dt("Error enabling index !index.", $vars), 'error'); } } else { - drush_log(dt("The index !index is already enabled.", array('!index' => $index->name)), 'error'); + drush_log(dt("The index !index is already enabled.", $vars), 'error'); } } } @@ -198,16 +211,23 @@ function drush_search_api_disable($index_id = NULL) { return; } foreach ($indexes as $index) { + $vars = array('!index' => $index->name); if ($index->enabled) { - if (search_api_index_disable($index->id)) { - drush_log(dt("The index !index was successfully disabled.", array('!index' => $index->name)), 'ok'); + $success = FALSE; + try { + if ($success = search_api_index_disable($index->id)) { + drush_log(dt("The index !index was successfully disabled.", $vars), 'ok'); + } + } + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); } - else { - drush_log(dt("Error disabling index !index.", array('!index' => $index->name)), 'error'); + if (!$success) { + drush_log(dt("Error disabling index !index.", $vars), 'error'); } } else { - drush_log(dt("The index !index is already disabled.", array('!index' => $index->name)), 'error'); + drush_log(dt("The index !index is already disabled.", $vars), 'error'); } } } @@ -266,7 +286,13 @@ function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = N } foreach ($indexes as $index) { // Get the number of remaing items to index. - $datasource = $index->datasource(); + try { + $datasource = $index->datasource(); + } + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); + continue; + } $index_status = $datasource->getIndexStatus($index); $remaining = $index_status['total'] - $index_status['indexed']; if ($remaining <= 0) { diff --git a/search_api.install b/search_api.install index 86812a4c..6db9ebb3 100644 --- a/search_api.install +++ b/search_api.install @@ -349,8 +349,13 @@ function search_api_enable() { } } foreach ($types as $type => $indexes) { - $controller = search_api_get_datasource_controller($type); - $controller->startTracking($indexes); + try { + $controller = search_api_get_datasource_controller($type); + $controller->startTracking($indexes); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } } diff --git a/search_api.module b/search_api.module index 000cadb2..2661f349 100644 --- a/search_api.module +++ b/search_api.module @@ -275,7 +275,7 @@ function search_api_theme() { 'options' => array(), 'fields' => array(), 'indexed_items' => 0, - 'on_server' => 0, + 'on_server' => NULL, 'total_items' => 0, 'status' => ENTITY_CUSTOM, 'read_only' => 0, @@ -662,9 +662,21 @@ function search_api_search_api_index_update(SearchApiIndex $index) { } if ($index->server) { - $new_server = $index->server(TRUE); - // If the server is enabled, we call addIndex(); otherwise, we save the task. - $new_server->addIndex($index); + try { + $new_server = $index->server(TRUE); + // If the server is enabled, we call addIndex(); otherwise, we save the task. + $new_server->addIndex($index); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + // If the new server doesn't exist, we remove the index from all + // servers. Note that saving an entity in its own update hook is usually + // a recipe for disaster, but since we are only doing this if a server + // is set and remove the server here before saving, it should be safe + // enough. + $index->server = NULL; + $index->save(); + } } // We also have to re-index all content. @@ -1098,7 +1110,14 @@ function search_api_track_item_insert($type, array $item_ids) { return; } - search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes); + try { + search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes); + } + catch (SearchApiException $e) { + $vars['%item_type'] = $type; + watchdog_exception('search_api', $e, '%type while inserting items of type %item_type: !message in %function (line %line of %file).', $vars); + return; + } foreach ($indexes as $index) { if (!empty($index->options['index_directly'])) { @@ -1128,19 +1147,26 @@ function search_api_track_item_change($type, array $item_ids) { if (!$indexes) { return; } - search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes); - foreach ($indexes as $index) { - if (!empty($index->options['index_directly'])) { - // For indexes with the index_directly option set, queue the items to be - // indexed at the end of the request. - try { - search_api_index_specific_items_delayed($index, $item_ids); - } - catch (SearchApiException $e) { - watchdog_exception('search_api', $e); + try { + search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes); + foreach ($indexes as $index) { + if (!empty($index->options['index_directly'])) { + // For indexes with the index_directly option set, queue the items to be + // indexed at the end of the request. + try { + search_api_index_specific_items_delayed($index, $item_ids); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } } } + catch (SearchApiException $e) { + $vars['%item_type'] = $type; + watchdog_exception('search_api', $e, '%type while updating items of type %item_type: !message in %function (line %line of %file).', $vars); + return; + } } /** @@ -1158,7 +1184,12 @@ function search_api_track_item_change($type, array $item_ids) { * the Drupal 8 version of this module. */ function search_api_track_item_queued(SearchApiIndex $index, array $item_ids) { - $index->datasource()->trackItemQueued($item_ids, $index); + try { + $index->datasource()->trackItemQueued($item_ids, $index); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } /** @@ -1191,16 +1222,27 @@ function search_api_track_item_delete($type, array $item_ids) { ); $indexes = search_api_index_load_multiple(FALSE, $conditions); if ($indexes) { - search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes); + try { + search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes); + } + catch (SearchApiException $e) { + $vars['%item_type'] = $type; + watchdog_exception('search_api', $e, '%type while deleting items of type %item_type: !message in %function (line %line of %file).', $vars); + } } // Then, delete it from all servers. Servers of disabled indexes have to be // considered, too! unset($conditions['enabled']); foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { - if ($index->server) { - $server = $index->server(); - $server->deleteItems($item_ids, $index); + try { + if ($server = $index->server()) { + $server->deleteItems($item_ids, $index); + } + } + catch (Exception $e) { + $vars['%item_type'] = $type; + watchdog_exception('search_api', $e, '%type while deleting items of type %item_type: !message in %function (line %line of %file).', $vars); } } } @@ -1495,10 +1537,7 @@ function search_api_index_specific_items(SearchApiIndex $index, array $ids) { // some specific setups. $type = search_api_get_item_type_info($index->item_type); $type = $type ? $type['name'] : $index->item_type; - watchdog('search_api', - "Error during indexing: invalid item loaded for @type with ID @id.", - array('@id' => $id, '@type' => $type), - WATCHDOG_WARNING); + watchdog('search_api', "Error during indexing: invalid item loaded for @type with ID @id.", array('@id' => $id, '@type' => $type), WATCHDOG_WARNING); } } $indexed = $items ? $index->index($cloned_items) : array(); @@ -1578,25 +1617,14 @@ function search_api_get_items_to_index(SearchApiIndex $index, $limit = -1) { * @param $id * The ID or machine name of the index to execute the search on. * @param $options - * An associative array of options. The following are recognized: - * - filters: Either a SearchApiQueryFilterInterface object or an array of - * filters used to filter the search. - * - sort: An array of sort directives of the form $field => $order, where - * $order is either 'ASC' or 'DESC'. - * - offset: The position of the first returned search results relative to the - * whole result in the index. - * - limit: The maximum number of search results to return. -1 means no limit. - * - 'query class': The query class to use. Must be a subtype of - * SearchApiQueryInterface. - * - conjunction: The type of conjunction to use for this query - either - * 'AND' or 'OR'. 'AND' by default. - * - 'parse mode': The mode with which to parse the $keys variable, if it - * is set and not already an array. See SearchApiQuery::parseModes() for - * parse modes recognized by the SearchApiQuery class. - * Subclasses might define additional modes. + * An associative array of options to be passed to + * SearchApiQueryInterface::__construct(). * * @return SearchApiQueryInterface * An object for searching on the specified index. + * + * @throws SearchApiException + * If the index is unknown or disabled, or some other error was encountered. */ function search_api_query($id, array $options = array()) { $index = search_api_index_load($id); @@ -1607,9 +1635,10 @@ function search_api_query($id, array $options = array()) { } /** - * Static store for the searches executed on the current page. Can either be - * used to store an executed search, or to retrieve a previously stored - * search. + * Stores or retrieves a search executed in this page request. + * + * Static storage for the searches executed during the current page request. Can + * used to store an executed search, or to retrieve a previously stored search. * * @param $search_id * For pages displaying multiple searches, an optional ID identifying the @@ -2556,12 +2585,18 @@ function search_api_index_url(SearchApiIndex $index) { * @param SearchApiIndex $index * The index whose server should be returned. * - * @return SearchApiServer + * @return SearchApiServer|null * The server this index currently resides on, or NULL if the index is * currently unassigned. */ function search_api_index_get_server(SearchApiIndex $index) { - return $index->server(); + try { + return $index->server(); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + return NULL; + } } /** @@ -2638,14 +2673,14 @@ function search_api_index_edit_fields($id, array $fields) { /** * Enables a search index. * - * @param $id + * @param string|int $id * The ID or machine name of the index to enable. * - * @throws SearchApiException - * If the index' server isn't enabled. - * - * @return + * @return int|false * 1 on success, 0 or FALSE on failure. + * + * @throws SearchApiException + * If the index's server doesn't exist. */ function search_api_index_enable($id) { $index = search_api_index_load($id, TRUE); @@ -2656,11 +2691,14 @@ function search_api_index_enable($id) { /** * Disables a search index. * - * @param $id + * @param string|int $id * The ID or machine name of the index to disable. * - * @return + * @return int|false * 1 on success, 0 or FALSE on failure. + * + * @throws SearchApiException + * If the index's server doesn't exist. */ function search_api_index_disable($id) { $index = search_api_index_load($id, TRUE); @@ -2808,6 +2846,9 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ * @return int * The number of items found on the server for this index, if the latter is * enabled. 0 otherwise. + * + * @throws SearchApiException + * If an error prevented the search from completing. */ function _search_api_get_items_on_server(SearchApiIndex $index) { if (!$index->enabled) { diff --git a/search_api.rules.inc b/search_api.rules.inc index eef4cc08..37143f87 100644 --- a/search_api.rules.inc +++ b/search_api.rules.inc @@ -52,6 +52,9 @@ function _search_api_rules_access() { * Rules action for indexing an item. */ function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiIndex $index = NULL, $index_immediately = TRUE) { + // If we do not have an index, we need to guess the item type to use. + // @todo Since this can only be used with entities anyways, we can just loop + // over the item type information and use all types with that entity type. $type = $wrapper->type(); $item_ids = array($wrapper->getIdentifier()); @@ -61,6 +64,7 @@ function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiI } if ($index) { + $type = $index->item_type; $indexes = array($index); } else { From fcb64742e9fd38e7fe0729c7835acbf0c4c6312c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Dec 2013 12:42:35 +0100 Subject: [PATCH 086/278] Issue #2150779 by hefox: Fixed "Overridden" detection for index features. --- CHANGELOG.txt | 1 + includes/index_entity.inc | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3388b363..61acf160 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2150779 by hefox: Fixed "Overridden" detection for index features. - #1227702 by drunken monkey: Improved error handling. Search API 1.11 (12/25/2013): diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 10d5c901..7afb9123 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -264,6 +264,10 @@ class SearchApiIndex extends Entity { $this->server = NULL; $this->enabled = FALSE; } + if (!empty($this->options['fields'])) { + ksort($this->options['fields']); + } + $this->resetCaches(); return parent::save(); From 7b9a3c7665b11f7b1ce598507f153b43fa057820 Mon Sep 17 00:00:00 2001 From: idebr Date: Tue, 25 Feb 2014 15:18:47 +0100 Subject: [PATCH 087/278] Issue #2168713 by idebr: Fixed highlighting of keys containing slashes. --- CHANGELOG.txt | 1 + includes/processor_highlight.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 61acf160..e3dcd269 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2168713 by idebr: Fixed highlighting of keys containing slashes. - #2150779 by hefox: Fixed "Overridden" detection for index features. - #1227702 by drunken monkey: Improved error handling. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index eda797c8..b92aeca7 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -309,7 +309,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { // Locate a keyword (position $p, always >0 because $text starts with a // space). $p = 0; - if (preg_match('/' . self::$boundary . $key . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) { + if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) { $p = $match[0][1]; } // Now locate a space in front (position $q) and behind it (position $s), @@ -395,7 +395,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { */ protected function highlightField($text, array $keys) { $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; - $keys = implode('|', array_map('preg_quote', $keys)); + $keys = implode('|', array_map('preg_quote', $keys, array('/'))); $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); return substr($text, 1, -1); } From 6cc53e4f56fc3fae398fa65e93ed51b3d6a20c06 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 7 Mar 2014 17:02:13 +0100 Subject: [PATCH 088/278] Fixed small comment typo. --- contrib/search_api_views/includes/handler_filter_entity.inc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index 7fb2e240..0b836894 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -90,7 +90,8 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi // Set the correct default value in case the admin-set value is used (and a // value is present). The value is used if the form is either not exposed, - // or the exposed form wasn't submitted yet (there is + // or the exposed form wasn't submitted yet. (There doesn't seem to be an + // easier way to check for that.) if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) { $form['value']['#default_value'] = $this->ids_to_strings($this->value); } From 3959b06be79ba2f7f311929940e7d773fa61e871 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 10 Mar 2014 16:22:52 +0100 Subject: [PATCH 089/278] Follow-up to #2168713 by drunken monkey: Fixed highlighting of keys containing slashes. --- includes/processor_highlight.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index b92aeca7..57e03168 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -395,7 +395,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { */ protected function highlightField($text, array $keys) { $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; - $keys = implode('|', array_map('preg_quote', $keys, array('/'))); + $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); return substr($text, 1, -1); } From a001ef501cab43b70bdf8c10319c44d68ac38f8c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 11 Mar 2014 12:37:50 +0100 Subject: [PATCH 090/278] Issue #2198261 by drunken monkey: Fixed fatal error on view editing. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/handler_filter_entity.inc | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e3dcd269..a4021204 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2198261 by drunken monkey: Fixed fatal error on view editing. - #2168713 by idebr: Fixed highlighting of keys containing slashes. - #2150779 by hefox: Fixed "Overridden" detection for index features. - #1227702 by drunken monkey: Improved error handling. diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index 0b836894..ca0913da 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -176,6 +176,9 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi * {@inheritdoc} */ public function admin_summary() { + if (!is_array($this->value)) { + $this->value = $this->value ? array($this->value) : array(); + } $value = $this->value; $this->value = empty($value) ? '' : $this->ids_to_strings($value); $ret = parent::admin_summary(); From a12513ed25206e5a76a79a872fba64fbc7ea3d93 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 17 Mar 2014 16:50:41 +0100 Subject: [PATCH 091/278] Corrected a tiny comment typo. --- includes/query.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/query.inc b/includes/query.inc index e9ccc207..c6631162 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -366,7 +366,7 @@ class SearchApiQuery implements SearchApiQueryInterface { /** * The index's machine name. * - * used during serialization to avoid serializing the whole index object. + * Used during serialization to avoid serializing the whole index object. * * @var string */ From 56d91816566958d613cfae1e60e8fe0f6a382bb0 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 19 Mar 2014 12:21:43 +0100 Subject: [PATCH 092/278] Issue #2187487 by drunken monkey: Fixed admin summary of language filter. --- CHANGELOG.txt | 1 + .../includes/handler_filter_language.inc | 22 +++++-------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a4021204..c0b22ecd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2187487 by drunken monkey: Fixed admin summary of language filter. - #2198261 by drunken monkey: Fixed fatal error on view editing. - #2168713 by idebr: Fixed highlighting of keys containing slashes. - #2150779 by hefox: Fixed "Overridden" detection for index features. diff --git a/contrib/search_api_views/includes/handler_filter_language.inc b/contrib/search_api_views/includes/handler_filter_language.inc index 399f80c1..a7de5f94 100644 --- a/contrib/search_api_views/includes/handler_filter_language.inc +++ b/contrib/search_api_views/includes/handler_filter_language.inc @@ -14,26 +14,14 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions { /** - * Provide a form for setting options. + * {@inheritdoc} */ - public function value_form(&$form, &$form_state) { - parent::value_form($form, $form_state); - $form['value']['#options'] = array( + protected function get_value_options() { + parent::get_value_options(); + $this->value_options = array( 'current' => t("Current user's language"), 'default' => t('Default site language'), - ) + $form['value']['#options']; - } - - /** - * Provides a summary of this filter's value for the admin UI. - */ - public function admin_summary() { - $tmp = $this->definition['options']; - $this->definition['options']['current'] = t('current'); - $this->definition['options']['default'] = t('default'); - $ret = parent::admin_summary(); - $this->definition['options'] = $tmp; - return $ret; + ) + $this->value_options; } /** From 67c8e2179b7c9627ad096ffb53d873f6faa11e58 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 13 Apr 2014 21:31:49 +0200 Subject: [PATCH 093/278] Issue #1888174 by drunken monkey, ipallian: Fixed problems with date facets. --- CHANGELOG.txt | 1 + .../plugins/facetapi/query_type_date.inc | 152 ++++++++++++------ .../search_api_facetapi.module | 130 ++++++++++++++- 3 files changed, 235 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c0b22ecd..1d9f802c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1888174 by drunken monkey, ipallian: Fixed problems with date facets. - #2187487 by drunken monkey: Fixed admin summary of language filter. - #2198261 by drunken monkey: Fixed fatal error on view editing. - #2168713 by idebr: Fixed highlighting of keys containing slashes. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index b045fc8f..fd93de41 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -57,14 +57,94 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue if ($active = $this->adapter->getActiveItems($this->facet)) { $item = end($active); $field = $this->facet['field']; - $regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE); - $filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']); + $filter = $this->createRangeFilter($item['value']); $this->addFacetFilter($query, $field, $filter); } } + /** + * Rewrites the handler-specific date range syntax to the normal facet syntax. + * + * @param $value + * The user-facing facet value. + * + * @return string + * A facet to add as a filter, in the format used internally in this module. + */ + protected function createRangeFilter($value) { + // Gets the granularity. Ignore any filters passed directly from the server + // (range or missing). We always create filters starting with a year. + if (!$value || !ctype_digit($value[0])) { + return $value; + } + + $parts = explode('-', $value); + $date = new DateTime(); + switch (count($parts)) { + case 1: + $date->setDate($parts[0], 1, 1); + $date->setTime(0, 0, 0); + $lower = $date->format('U'); + $date->setDate($parts[0] + 1, 1, 1); + $date->setTime(0, 0, -1); + $upper = $date->format('U'); + break; + + case 2: + // Luckily, $month = 13 is treated as January of next year. (The same + // goes for all other parameters.) We use the inverse trick for the + // seconds of the upper bound, since that's inclusive and we want to + // stop at a second before the next segment starts. + $date->setDate($parts[0], $parts[1], 1); + $date->setTime(0, 0, 0); + $lower = $date->format('U'); + $date->setDate($parts[0], $parts[1] + 1, 1); + $date->setTime(0, 0, -1); + $upper = $date->format('U'); + break; + + case 3: + $date->setDate($parts[0], $parts[1], $parts[2]); + $date->setTime(0, 0, 0); + $lower = $date->format('U'); + $date->setDate($parts[0], $parts[1], $parts[2] + 1); + $date->setTime(0, 0, -1); + $upper = $date->format('U'); + break; + + case 4: + $date->setDate($parts[0], $parts[1], $parts[2]); + $date->setTime($parts[3], 0, 0); + $lower = $date->format('U'); + $date->setTime($parts[3] + 1, 0, -1); + $upper = $date->format('U'); + break; + + case 5: + $date->setDate($parts[0], $parts[1], $parts[2]); + $date->setTime($parts[3], $parts[4], 0); + $lower = $date->format('U'); + $date->setTime($parts[3], $parts[4] + 1, -1); + $upper = $date->format('U'); + break; + + case 6: + $date->setDate($parts[0], $parts[1], $parts[2]); + $date->setTime($parts[3], $parts[4], $parts[5]); + return $date->format('U'); + + default: + return $value; + } + + return "[$lower TO $upper]"; + } + /** * Replacement callback for replacing ISO dates with timestamps. + * + * Not used anymore, but kept for backwards compatibility with potential + * subclasses. */ public function replaceDateString($matches) { return strtotime($matches[0]); @@ -86,15 +166,9 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue $build = array(); $search = search_api_current_search($search_id); $results = $search[1]; - if (!$results['result count']) { - return array(); - } // Gets total number of documents matched in search. $total = $results['result count']; - // Most of the code below is copied from search_facetapi's implementation of - // this method. - // Executes query, iterates over results. if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) { $values = $results['search_api_facets'][$this->facet['name']]; @@ -113,13 +187,6 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue } } else { - $filter = substr($value['filter'], 1, -1); - $pos = strpos($filter, ' '); - if ($pos !== FALSE) { - $lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY); - $upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY); - $filter = '[' . $lower . ' TO ' . $upper . ']'; - } $build[$filter]['#count'] = $value['count']; } } @@ -128,23 +195,28 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue // Get the finest level of detail we're allowed to drill down to. $settings = $facet->getSettings()->settings; - $granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE; + $max_granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE; // Gets active facets, starts building hierarchy. - $parent = $gap = NULL; + $parent = $granularity = NULL; $active_items = $this->adapter->getActiveItems($this->facet); foreach ($active_items as $value => $item) { // If the item is active, the count is the result set count. $build[$value] = array('#count' => $total); - // Gets next "gap" increment. - if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) { + // Gets next "gap" increment. Ignore any filters passed directly from the + // server (range or missing). We always create filters starting with a + // year. + $value = "$value"; + if (!$value || !ctype_digit($value[0])) { + continue; + } + + $granularity = search_api_facetapi_date_get_granularity($value); + if (!$granularity) { continue; } - $start = substr($value, 1, $pos); - $end = substr($value, $pos + 4, -1); - $date_gap = facetapi_get_date_gap($start, $end); - $gap = facetapi_get_next_date_gap($date_gap, $granularity); + $granularity = facetapi_get_next_date_gap($granularity, $max_granularity); // If there is a previous item, there is a parent, uses a reference so the // arrays are populated when they are updated. @@ -156,6 +228,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue // Stores the last value iterated over. $parent = $value; } + if (empty($raw_values)) { return $build; } @@ -165,7 +238,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue $timestamps = array_keys($raw_values); if (NULL === $parent) { if (count($raw_values) > 1) { - $gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps)); + $granularity = facetapi_get_timestamp_gap(min($timestamps), max($timestamps)); // Array of numbers used to determine whether the next gap is smaller than // the minimum gap allowed in the drilldown. $gap_numbers = array( @@ -178,36 +251,20 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue ); // Gets gap numbers for both the gap and minimum gap, checks if the gap // is within the limit set by the $granularity parameter. - if ($gap_numbers[$gap] < $gap_numbers[$granularity]) { - $gap = $granularity; + if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) { + $granularity = $max_granularity; } } else { - $gap = $granularity; - } - } - - // Converts all timestamps to dates in ISO 8601 format. - $dates = array(); - foreach ($timestamps as $timestamp) { - $dates[$timestamp] = facetapi_isodate($timestamp, $gap); - } - - // Treat each date as the range start and next date as the range end. - $range_end = array(); - $previous = NULL; - foreach (array_unique($dates) as $date) { - if (NULL !== $previous) { - $range_end[$previous] = facetapi_get_next_date_increment($previous, $gap); + $granularity = $max_granularity; } - $previous = $date; } - $range_end[$previous] = facetapi_get_next_date_increment($previous, $gap); - // Groups dates by the range they belong to, builds the $build array - // with the facet counts and formatted range values. + // Groups dates by the range they belong to, builds the $build array with + // the facet counts and formatted range values. + $format = search_api_facetapi_date_get_granularity_format($granularity); foreach ($raw_values as $value => $count) { - $new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']'; + $new_value = date($format, $value); if (!isset($build[$new_value])) { $build[$new_value] = array('#count' => $count); } @@ -226,4 +283,5 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue return $build; } + } diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index 0e3da91c..5b6df861 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -97,7 +97,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) { 'date' => array( 'query type' => 'date', 'map options' => array( - 'map callback' => 'facetapi_map_date', + 'map callback' => 'search_api_facetapi_map_date', ), ), ); @@ -454,3 +454,131 @@ function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_ $cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':'; cache_clear_all($cid, 'cache', TRUE); } + +/** + * Computes the granularity of a date facet filter. + * + * @param $filter + * The filter value to examine. + * + * @return string|null + * Either one of the FACETAPI_DATE_* constants corresponding to the + * granularity of the filter, or NULL if it couldn't be computed. + */ +function search_api_facetapi_date_get_granularity($filter) { + // Granularity corresponds to number of dashes in filter value. + $units = array( + FACETAPI_DATE_YEAR, + FACETAPI_DATE_MONTH, + FACETAPI_DATE_DAY, + FACETAPI_DATE_HOUR, + FACETAPI_DATE_MINUTE, + FACETAPI_DATE_SECOND, + ); + $count = substr_count($filter, '-'); + return isset($units[$count]) ? $units[$count] : NULL; +} + +/** + * Returns the date format used for a given granularity. + * + * @param $granularity + * One of the FACETAPI_DATE_* constants. + * + * @return string + * The date format used for the given granularity. + */ +function search_api_facetapi_date_get_granularity_format($granularity) { + $formats = array( + FACETAPI_DATE_YEAR => 'Y', + FACETAPI_DATE_MONTH => 'Y-m', + FACETAPI_DATE_DAY => 'Y-m-d', + FACETAPI_DATE_HOUR => 'Y-m-d-H', + FACETAPI_DATE_MINUTE => 'Y-m-d-H-i', + FACETAPI_DATE_SECOND => 'Y-m-d-H-i-s', + ); + return $formats[$granularity]; +} + +/** + * Constructs labels for date facet filter values. + * + * @param array $values + * The date facet filter values, as used in URL parameters. + * @param array $options + * (optional) Options for creating the mapping. The following options are + * recognized: + * - format callback: A callback for creating a label for a timestamp. The + * function signature is like search_api_facetapi_format_timestamp(), + * receiving a timestamp and one of the FACETAPI_DATE_* constants as the + * parameters and returning a human-readable label. + * + * @return array + * An array of labels for the given facet filters. + */ +function search_api_facetapi_map_date(array $values, array $options = array()) { + $map = array(); + foreach ($values as $value) { + // Ignore any filters passed directly from the server (range or missing). We + // always create filters starting with a year. + $value = "$value"; + if (!$value || !ctype_digit($value[0])) { + continue; + } + + // Get the granularity of the filter. + $granularity = search_api_facetapi_date_get_granularity($value); + if (!$granularity) { + continue; + } + + // For years, the URL value is already the label. + if ($granularity == FACETAPI_DATE_YEAR) { + $map[$value] = $value; + continue; + } + + // Otherwise, parse the timestamp from the known format and format it as a + // label. + $format = search_api_facetapi_date_get_granularity_format($granularity); + $date = DateTime::createFromFormat($format, $value); + if (!$date) { + continue; + } + $format_callback = 'search_api_facetapi_format_timestamp'; + if (!empty($options['format callback']) && is_callable($options['format callback'])) { + $format_callback = $options['format callback']; + } + $map[$value] = call_user_func($format_callback, $date->format('U'), $granularity); + } + return $map; +} + +/** + * Format a date according to the default timezone and the given precision. + * + * @param int $timestamp + * An integer containing the Unix timestamp being formated. + * @param string $precision + * A string containing the formatting precision. See the FACETAPI_DATE_* + * constants for valid values. + * + * @return string + * A human-readable representation of the timestamp. + */ +function search_api_facetapi_format_timestamp($timestamp, $precision = FACETAPI_DATE_YEAR) { + $formats = array( + FACETAPI_DATE_YEAR => 'Y', + FACETAPI_DATE_MONTH => 'F Y', + FACETAPI_DATE_DAY => 'F j, Y', + FACETAPI_DATE_HOUR => 'H:__', + FACETAPI_DATE_MINUTE => 'H:i', + FACETAPI_DATE_SECOND => 'H:i:s', + ); + + if (!isset($formats[$precision])) { + $precision = FACETAPI_DATE_YEAR; + } + + return format_date($timestamp, 'custom', $formats[$precision]); +} From 73741e4931e034a784881677c879e4610b8ced13 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 14 Apr 2014 09:46:48 +0200 Subject: [PATCH 094/278] Issue #2219563 by drunken monkey: Added __toString() methods for queries and filters. --- CHANGELOG.txt | 2 ++ includes/query.inc | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1d9f802c..0a04f4e4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2219563 by drunken monkey: Added __toString() methods for queries and + filters. - #1888174 by drunken monkey, ipallian: Fixed problems with date facets. - #2187487 by drunken monkey: Fixed admin summary of language filter. - #2198261 by drunken monkey: Fixed fatal error on view editing. diff --git a/includes/query.inc b/includes/query.inc index c6631162..8c63bebb 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -811,6 +811,31 @@ class SearchApiQuery implements SearchApiQueryInterface { $this->filter = clone $this->filter; } + /** + * Implements the magic __toString() method to simplify debugging. + */ + public function __toString() { + $ret = 'Index: ' . $this->index->machine_name . "\n"; + $ret .= 'Keys: ' . str_replace("\n", "\n ", var_export($this->orig_keys, TRUE)) . "\n"; + if (isset($this->keys)) { + $ret .= 'Parsed keys: ' . str_replace("\n", "\n ", var_export($this->keys, TRUE)) . "\n"; + $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n"; + } + if ($filter = (string) $this->filter) { + $filter = str_replace("\n", "\n ", $filter); + $ret .= "Filters:\n $filter\n"; + } + if ($this->sort) { + $sort = array(); + foreach ($this->sort as $field => $order) { + $sort[] = "$field $order"; + } + $ret .= 'Sorting: ' . implode(', ', $sort) . "\n"; + } + $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n"; + return $ret; + } + } /** @@ -1010,4 +1035,24 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { } } + /** + * Implements the magic __toString() method to simplify debugging. + */ + public function __toString() { + // Special case for a single, nested filter: + if (count($this->filters) == 1 && is_object($this->filters[0])) { + return (string) $this->filters[0]; + } + $ret = array(); + foreach ($this->filters as $filter) { + if (is_object($filter)) { + $ret[] = "[\n " . str_replace("\n", "\n ", (string) $filter) . "\n ]"; + } + else { + $ret[] = "$filter[0] $filter[2] " . str_replace("\n", "\n ", var_export($filter[1], TRUE)); + } + } + return $ret ? ' ' . implode("\n{$this->conjunction}\n ", $ret) : ''; + } + } From e424e73dcd7151324c3afaa9ab977fda80b73fce Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 15:41:02 +0200 Subject: [PATCH 095/278] Issue #2169455 by drunken monkey: Fixed "undefined index" in search_api_update_7116(). --- CHANGELOG.txt | 2 ++ search_api.install | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0a04f4e4..15e6758a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2169455 by drunken monkey: Fixed "undefined index" in + search_api_update_7116(). - #2219563 by drunken monkey: Added __toString() methods for queries and filters. - #1888174 by drunken monkey, ipallian: Fixed problems with date facets. diff --git a/search_api.install b/search_api.install index 6db9ebb3..850b8280 100644 --- a/search_api.install +++ b/search_api.install @@ -966,6 +966,10 @@ function search_api_update_7116() { $insert = db_insert('search_api_task') ->fields(array('server_id', 'type', 'index_id', 'data')); foreach ($tasks as $task) { + $task += array( + 'index_id' => NULL, + 'data' => NULL, + ); $insert->values($task); } $insert->execute(); From 17350204a8d3a7bf9d5240be401df9f026f4ecfa Mon Sep 17 00:00:00 2001 From: freakalis Date: Mon, 12 May 2014 16:24:42 +0200 Subject: [PATCH 096/278] Issue #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to Highlighting processor. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 36 +++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 15e6758a..54e96b62 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to + Highlighting processor. - #2169455 by drunken monkey: Fixed "undefined index" in search_api_update_7116(). - #2219563 by drunken monkey: Added __toString() methods for queries and diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 57e03168..ee536004 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -53,6 +53,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { 'excerpt' => TRUE, 'excerpt_length' => 256, 'highlight' => 'always', + 'exclude_fields' => array(), ); $form['prefix'] = array( @@ -87,6 +88,22 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { ), ), ); + // Exclude certain fulltextfields + $fields = $this->index->getFields(); + $fulltext_fields = array(); + foreach ($this->index->getFulltextFields() as $field) { + if (isset($fields[$field])) { + $fulltext_fields[$field] = $fields[$field]['name'] . ' (' . $field . ')'; + } + } + $form['exclude_fields'] = array( + '#type' => 'checkboxes', + '#title' => t('Exclude fields from excerpt'), + '#description' => t('Exclude certain fulltext fields from being displayed in the excerpt.'), + '#options' => $fulltext_fields, + '#default_value' => $this->options['exclude_fields'], + '#attributes' => array('class' => array('search-api-checkboxes-list')), + ); $form['highlight'] = array( '#type' => 'select', '#title' => t('Highlight returned field data'), @@ -106,7 +123,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * {@inheritdoc} */ public function configurationFormValidate(array $form, array &$values, array &$form_state) { - // Overridden so $form['fields'] is not checked. + $values['exclude_fields'] = array_filter($values['exclude_fields']); } /** @@ -117,10 +134,18 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { return; } + $fulltext_fields = $this->index->getFulltextFields(); + if (!empty($this->options['exclude_fields'])) { + $fulltext_fields = drupal_map_assoc($fulltext_fields); + foreach ($this->options['exclude_fields'] as $field) { + unset($fulltext_fields[$field]); + } + } + foreach ($response['results'] as $id => &$result) { if ($this->options['excerpt']) { $text = array(); - $fields = $this->getFulltextFields($response['results'], $id); + $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields); foreach ($fields as $data) { if (is_array($data)) { $text = array_merge($text, $data); @@ -132,7 +157,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { $result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys); } if ($this->options['highlight'] != 'never') { - $fields = $this->getFulltextFields($response['results'], $id, $this->options['highlight'] == 'always'); + $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields, $this->options['highlight'] == 'always'); foreach ($fields as $field => $data) { if (is_array($data)) { foreach ($data as $i => $text) { @@ -155,6 +180,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * @param int|string $i * The index in the results array of the result whose data should be * returned. + * @param array $fulltext_fields + * The fulltext fields from which the excerpt should be created. * @param bool $load * TRUE if the item should be loaded if necessary, FALSE if only fields * already returned in the results should be used. @@ -163,7 +190,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * An array containing fulltext field names mapped to the text data * contained in them for the given result. */ - protected function getFulltextFields(array &$results, $i, $load = TRUE) { + protected function getFulltextFields(array &$results, $i, array $fulltext_fields, $load = TRUE) { global $language; $data = array(); @@ -171,7 +198,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { // Act as if $load is TRUE if we have a loaded item. $load |= !empty($result['entity']); $result += array('fields' => array()); - $fulltext_fields = $this->index->getFulltextFields(); // We only need detailed fields data if $load is TRUE. $fields = $load ? $this->index->getFields() : array(); $needs_extraction = array(); From 5092cf133b916b8f1dab7d6e637d68f5da3f6815 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 16:26:16 +0200 Subject: [PATCH 097/278] Issue #2198791 by drunken monkey: Fixed empty Views entity filters. --- CHANGELOG.txt | 1 + .../includes/handler_filter_entity.inc | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 54e96b62..720d5689 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2198791 by drunken monkey: Fixed empty Views entity filters. - #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to Highlighting processor. - #2169455 by drunken monkey: Fixed "undefined index" in diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index ca0913da..ffae8ad4 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -103,11 +103,13 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi public function value_validate($form, &$form_state) { if (!empty($form['value'])) { $value = &$form_state['values']['options']['value']; - $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value); - $ids = $this->validate_entity_strings($form['value'], $values); + if (strlen($value)) { + $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value); + $ids = $this->validate_entity_strings($form['value'], $values); - if ($ids) { - $value = $ids; + if ($ids) { + $value = $ids; + } } } } @@ -136,6 +138,7 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi return; } + $this->validated_exposed_input = FALSE; $identifier = $this->options['expose']['identifier']; $input = $form_state['values'][$identifier]; @@ -144,14 +147,14 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi $input = $this->options['group_info']['group_items'][$input]['value']; } + if (!strlen($input)) { + return; + } $values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input); if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) { $this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values); } - else { - $this->validated_exposed_input = FALSE; - } } /** From 0325f59f54de7b930505ced3fdfbce9234d0e70c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 16:27:42 +0200 Subject: [PATCH 098/278] Issue pagination --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 22 ++++++++++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 720d5689..07d76f89 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- pagination - #2198791 by drunken monkey: Fixed empty Views entity filters. - #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to Highlighting processor. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 36bc2326..d84a1ad2 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -247,6 +247,16 @@ class SearchApiViewsQuery extends views_plugin_query { $view->init_pager(); $this->pager->query(); + // Views passes sometimes NULL and sometimes the integer 0 for "All" in a + // pager. If set to 0 items, a string "0" is passed. Therefore, we unset + // the limit if an empty value OTHER than a string "0" was passed. + if (!$this->limit && $this->limit !== '0') { + $this->limit = NULL; + } + // Set the range. (We always set this, as there might even be an offset if + // all items are shown.) + $this->query->range($this->offset, $this->limit); + // Set the search ID, if it was not already set. if ($this->query->getOption('search id') == get_class($this->query)) { $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display); @@ -304,16 +314,6 @@ class SearchApiViewsQuery extends views_plugin_query { // Trigger pager pre_execute(). $this->pager->pre_execute($this->query); - // Views passes sometimes NULL and sometimes the integer 0 for "All" in a - // pager. If set to 0 items, a string "0" is passed. Therefore, we unset - // the limit if an empty value OTHER than a string "0" was passed. - if (!$this->limit && $this->limit !== '0') { - $this->limit = NULL; - } - // Set the range. (We always set this, as there might even be an offset if - // all items are shown.) - $this->query->range($this->offset, $this->limit); - $start = microtime(TRUE); // Execute the search. @@ -342,7 +342,7 @@ class SearchApiViewsQuery extends views_plugin_query { catch (Exception $e) { $this->errors[] = $e->getMessage(); // Recursion to get the same error behaviour as above. - return $this->execute($view); + $this->execute($view); } } From 67bc21ab33d3a7851a22e04320fed1fe331b5bf0 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 16:29:16 +0200 Subject: [PATCH 099/278] Issue #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter. --- CHANGELOG.txt | 1 + includes/processor_html_filter.inc | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 07d76f89..3a88fa85 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter. - pagination - #2198791 by drunken monkey: Fixed empty Views entity filters. - #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to diff --git a/includes/processor_html_filter.inc b/includes/processor_html_filter.inc index 41777bd2..0e714c32 100644 --- a/includes/processor_html_filter.inc +++ b/includes/processor_html_filter.inc @@ -102,6 +102,8 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { } else { $value = strip_tags($text); + // Remove any multiple or leading/trailing spaces we might have introduced. + $value = preg_replace('/\s\s+/', ' ', trim($value)); } } @@ -109,8 +111,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { $ret = array(); while (($pos = strpos($text, '<')) !== FALSE) { if ($boost && $pos > 0) { + $token = html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'); + // Remove any multiple or leading/trailing spaces we might have introduced. + $token = preg_replace('/\s\s+/', ' ', trim($token)); $ret[] = array( - 'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'), + 'value' => $token, 'score' => $boost, ); } @@ -130,8 +135,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { } } if ($text) { + $token = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); + // Remove any multiple or leading/trailing spaces we might have introduced. + $token = preg_replace('/\s\s+/', ' ', trim($token)); $ret[] = array( - 'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'), + 'value' => $token, 'score' => $boost, ); $text = ''; From ca46ff9f73bdf27fc29c609716ef39bc7512ad28 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 16:33:22 +0200 Subject: [PATCH 100/278] Issue #2135697 by drunken monkey: Fixed handling of HTML attributes in the Highlighting processor. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3a88fa85..dd9d2113 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2135697 by drunken monkey: Fixed handling of HTML attributes in the + Highlighting processor. - #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter. - pagination - #2198791 by drunken monkey: Fixed empty Views entity filters. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index ee536004..c3521729 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -405,7 +405,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { $text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2]; $text = check_plain($text); - return $this->highlightField($text, $keys); + // Since we stripped the tags at the beginning, highlighting doesn't need to + // handle HTML anymore. + return $this->highlightField($text, $keys, FALSE); } /** @@ -415,11 +417,21 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * The text of the field. * @param array $keys * Search keywords entered by the user. + * @param bool $html + * Whether the text can contain HTML tags or not. In the former case, text + * inside tags (i.e., tag names and attributes) won't be highlighted. * * @return string * The field's text with all occurrences of search keywords highlighted. */ - protected function highlightField($text, array $keys) { + protected function highlightField($text, array $keys, $html = TRUE) { + if ($html) { + $texts = preg_split('#((?:"\']*|"[^"]*"|\'[^\']\')*>)+)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + for ($i = 0; $i < count($texts); $i += 2) { + $texts[$i] = $this->highlightField($texts[$i], $keys, FALSE); + } + return implode('', $texts); + } $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); From bfbe1ac40f812793eb247b418c7dbca97c9b1700 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 12 May 2014 18:08:07 +0200 Subject: [PATCH 101/278] Issue #2219553 by drunken monkey: Fixed Views fulltext filter operators. --- CHANGELOG.txt | 1 + .../search_api_views/includes/handler_filter_fulltext.inc | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd9d2113..8af327e4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2219553 by drunken monkey: Fixed Views fulltext filter operators. - #2135697 by drunken monkey: Fixed handling of HTML attributes in the Highlighting processor. - #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter. diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index 55226218..6b5cba12 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -156,8 +156,9 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex if ($filter) { $filter = $this->query->createFilter('OR'); + $op = $this->operator === 'NOT' ? '<>' : '='; foreach ($fields as $field) { - $filter->condition($field, $this->value, $this->operator); + $filter->condition($field, $this->value, $op); } $this->query->filter($filter); return; @@ -166,7 +167,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex // If the operator was set to OR or NOT, set OR as the conjunction. (It is // also set for NOT since otherwise it would be "not all of these words".) if ($this->operator != 'AND') { - $this->query->setOption('conjunction', $this->operator); + $this->query->setOption('conjunction', 'OR'); } $this->query->fields($fields); From 0859f9dddfcbd47be3ad458feb782da1fc18b581 Mon Sep 17 00:00:00 2001 From: rjacobs Date: Wed, 14 May 2014 18:04:21 +0200 Subject: [PATCH 102/278] Issue #2233749 by rjacobs, drunken monkey: Added drush support to change the server used by an index. --- CHANGELOG.txt | 2 + search_api.drush.inc | 93 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8af327e4..8e275b45 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2233749 by rjacobs, drunken monkey: Added drush support to change the server + used by an index. - #2219553 by drunken monkey: Fixed Views fulltext filter operators. - #2135697 by drunken monkey: Fixed handling of HTML attributes in the Highlighting processor. diff --git a/search_api.drush.inc b/search_api.drush.inc index d13dce1e..f04bef42 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -108,6 +108,19 @@ function search_api_drush_command() { 'aliases' => array('sapi-c'), ); + $items['search-api-set-index-server'] = array( + 'description' => 'Set the search server used by a given index.', + 'examples' => array( + 'drush search-api-set-index-server default_node_index my_solr_server' => dt('Set the !index index to use the !server server.', array('!index' => 'default_node_index', '!server' => 'my_solr_server')), + 'drush sapi-sis default_node_index my_solr_server' => dt('Alias to set the !index index to use the !server server.', array('!index' => 'default_node_index', '!server' => 'my_solr_server')), + ), + 'arguments' => array( + 'index_id' => dt('The numeric ID or machine name of an index.'), + 'server_id' => dt('The numeric ID or machine name of a server to set on the index.'), + ), + 'aliases' => array('sapi-sis'), + ); + return $items; } @@ -393,12 +406,54 @@ function drush_search_api_clear($index_id = NULL) { } /** - * Helper function to return an index or all indexes as an array. + * Set the server for a given index. + */ +function drush_search_api_set_index_server($index_id = NULL, $server_id = NULL) { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + // Make sure we have parameters to work with. + if (empty($index_id) || empty($server_id)) { + drush_log(dt('You must specify both an index and server.'), 'error'); + return; + } + // Fetch current index and server data. + $indexes = search_api_drush_get_index($index_id); + $servers = search_api_drush_get_server($server_id); + if (empty($indexes) || empty($servers)) { + // If the specified index or server can't be found, just return. An + // appropriate error message should have been printed already. + return; + } + // Set the new server on the index. + $success = FALSE; + $index = reset($indexes); + $server = reset($servers); + try { + $success = $index->update(array('server' => $server->machine_name)); + } + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); + } + if ($success === FALSE) { + drush_log(dt('There was an error setting index !index to use server !server.', array('!index' => $index->name, '!server' => $server->name)), 'error'); + } + elseif (!$success) { + drush_log(dt('Index !index was already using server !server.', array('!index' => $index->name, '!server' => $server->name)), 'ok'); + } + else { + drush_log(dt('Index !index has been set to use server !server and items have been queued for indexing.', array('!index' => $index->name, '!server' => $server->name)), 'ok'); + } +} + +/** + * Returns an index or all indexes as an array. * - * @param $index_id - * (optional) The provided index id. + * @param string|int|null $index_id + * (optional) The ID or machine name of the index to load. Defaults to + * loading all available indexes. * - * @return + * @return SearchApiIndex[] * An array of indexes. */ function search_api_drush_get_index($index_id = NULL) { @@ -413,7 +468,34 @@ function search_api_drush_get_index($index_id = NULL) { } /** - * Static lookup to prevent Drush 4 from running twice. + * Returns a server or all servers as an array. + * + * @param string|int|null $server_id + * (optional) The ID or machine name of the server to load. Defaults to + * loading all available servers. + * + * @return SearchApiServer[] + * An array of servers. + */ +function search_api_drush_get_server($server_id = NULL) { + $ids = isset($server_id) ? array($server_id) : FALSE; + $servers = search_api_server_load_multiple($ids); + if (empty($servers)) { + drush_set_error(dt('Invalid server_id or no servers present.')); + // @todo: Maybe add logic to print table of all servers. + } + return $servers; +} + +/** + * Does a static lookup to prevent Drush 4 from running twice. + * + * @param string $function + * The Drush function being called. + * + * @return bool + * TRUE if the function was already called in this Drush execution, FALSE + * otherwise. * * @see http://drupal.org/node/704848 */ @@ -423,4 +505,5 @@ function search_api_drush_static($function) { return TRUE; } $index[$function] = TRUE; + return FALSE; } From 22a52ab8986e08872a4a8692fcb60b6c614be749 Mon Sep 17 00:00:00 2001 From: justanothermark Date: Thu, 15 May 2014 14:56:18 +0200 Subject: [PATCH 103/278] Issue #2256891 by justanothermark: Fixed "0" entity labels. --- CHANGELOG.txt | 1 + contrib/search_api_facetapi/search_api_facetapi.module | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8e275b45..5c923629 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2256891 by justanothermark: Fixed "0" entity labels. - #2233749 by rjacobs, drunken monkey: Added drush support to change the server used by an index. - #2219553 by drunken monkey: Fixed Views fulltext filter operators. diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index 5b6df861..b7558be4 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -388,7 +388,7 @@ function _search_api_facetapi_facet_create_label(array $values, array $options) $entities = entity_load($type, $values); foreach ($entities as $id => $entity) { $label = entity_label($type, $entity); - if ($label) { + if ($label !== FALSE) { $map[$id] = $label; } } From 950e379f5f53df01811c1734ba4acb80ff219a46 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 23 May 2014 11:12:57 +0200 Subject: [PATCH 104/278] Issue #2265349 by drunken monkey: Marked _search_api_settings_equals() as deprecated. --- CHANGELOG.txt | 2 ++ search_api.module | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5c923629..b48e2078 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2265349 by drunken monkey: Marked _search_api_settings_equals() as + deprecated. - #2256891 by justanothermark: Fixed "0" entity labels. - #2233749 by rjacobs, drunken monkey: Added drush support to change the server used by an index. diff --git a/search_api.module b/search_api.module index 2661f349..b8a3cf16 100644 --- a/search_api.module +++ b/search_api.module @@ -1431,7 +1431,7 @@ function search_api_index_recalculate_fields($indexes = FALSE) { } // Use a more accurate method of determining if the fields settings are // equal to avoid needlessly re-indexing the whole index. - if (!_search_api_settings_equals($fields, $index->options['fields'])) { + if ($fields != $index->options['fields']) { $options = $index->options; $options['fields'] = $fields; $index->update(array('options' => $options)); @@ -1442,9 +1442,6 @@ function search_api_index_recalculate_fields($indexes = FALSE) { /** * Test two setting arrays (or individual settings) for equality. * - * While a simple == also works in some cases, this function takes into account - * that the order of keys (usually) doesn't matter in settings arrays. - * * @param mixed $setting1 * The first setting (array). * @param mixed $setting2 @@ -1452,6 +1449,8 @@ function search_api_index_recalculate_fields($indexes = FALSE) { * * @return bool * TRUE if both settings are identical, FALSE otherwise. + * + * @deprecated The simple "==" operator will achieve the same. */ function _search_api_settings_equals($setting1, $setting2) { if (!is_array($setting1) || !is_array($setting2)) { From 43cca3344d62cf401155ae60e77db98ef0256a18 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 23 May 2014 11:14:47 +0200 Subject: [PATCH 105/278] Adapted CHANGELOG.txt to 1.12 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b48e2078..98012c20 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.12 (05/23/2014): +----------------------------- - #2265349 by drunken monkey: Marked _search_api_settings_equals() as deprecated. - #2256891 by justanothermark: Fixed "0" entity labels. From 99b5e4bd314734a3dbe82caf8230c6a03ddb560a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 23 May 2014 11:15:20 +0200 Subject: [PATCH 106/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 98012c20..8a601705 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.12 (05/23/2014): ----------------------------- - #2265349 by drunken monkey: Marked _search_api_settings_equals() as From 52b91fefbc23a059f32c4293fa7e78b4ea368cef Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 23 May 2014 11:20:46 +0200 Subject: [PATCH 107/278] Issue #2204847 by drunken monkey, alanmackenzie: Fixed Views caching issues with pagination. --- CHANGELOG.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8a601705..eafaba4d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -12,7 +12,8 @@ Search API 1.12 (05/23/2014): - #2135697 by drunken monkey: Fixed handling of HTML attributes in the Highlighting processor. - #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter. -- pagination +- #2204847 by drunken monkey, alanmackenzie: Fixed Views caching issues with + pagination. - #2198791 by drunken monkey: Fixed empty Views entity filters. - #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to Highlighting processor. From c3206ecdd358099a1513f256e4ed6a16602922d8 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 18 Jun 2014 14:49:59 +0200 Subject: [PATCH 108/278] Issue #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string conversion in Highlighting processor. --- CHANGELOG.txt | 2 ++ includes/index_entity.inc | 2 +- includes/processor_highlight.inc | 33 +++++++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eafaba4d..e145f026 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string + conversion in Highlighting processor. Search API 1.12 (05/23/2014): ----------------------------- diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 7afb9123..745e9049 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -388,7 +388,7 @@ class SearchApiIndex extends Entity { return $this->datasource()->getEntityType(); } catch (SearchApiException $e) { - return ''; + return NULL; } } diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index c3521729..edfdb28b 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -154,7 +154,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { $text[] = $data; } } - $result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys); + + $result['excerpt'] = $this->createExcerpt($this->flattenArrayValues($text), $keys); } if ($this->options['highlight'] != 'never') { $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields, $this->options['highlight'] == 'always'); @@ -425,6 +426,10 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * The field's text with all occurrences of search keywords highlighted. */ protected function highlightField($text, array $keys, $html = TRUE) { + if (is_array($text)) { + $text = $this->flattenArrayValues($text); + } + if ($html) { $texts = preg_split('#((?:"\']*|"[^"]*"|\'[^\']\')*>)+)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); for ($i = 0; $i < count($texts); $i += 2) { @@ -433,9 +438,35 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { return implode('', $texts); } $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; + $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); return substr($text, 1, -1); } + /** + * Flattens a (possibly multidimensional) array into a string. + * + * @param array $array + * The array to flatten. + * @param string $glue + * The separator to insert between individual array items. + * + * @return string + * The glued string. + */ + protected function flattenArrayValues(array $array, $glue = "\n\n") { + $ret = array(); + foreach ($array as $item) { + if (is_array($item)) { + $ret[] = $this->flattenArrayValues($item, $glue); + } + else { + $ret[] = $item; + } + } + + return implode($glue, $ret); + } + } From 30d12b9f961645fbd4f8ca97048acb972cd5dddf Mon Sep 17 00:00:00 2001 From: idflood Date: Sun, 22 Jun 2014 12:03:00 +0200 Subject: [PATCH 109/278] Issue #2272983 by idflood, drunken monkey: Fixed Highlighting processor for queries without returned results. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e145f026..ff6fa44d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2272983 by idflood, drunken monkey: Fixed Highlighting processor for queries + without returned results. - #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string conversion in Highlighting processor. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index edfdb28b..232e652e 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -130,7 +130,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * {@inheritdoc} */ public function postprocessSearchResults(array &$response, SearchApiQuery $query) { - if (!$response['result count'] || !($keys = $this->getKeywords($query))) { + if (empty($response['results']) || !($keys = $this->getKeywords($query))) { return; } From 883af6561f4436397e513ae72f8b690439b69375 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 9 Jul 2014 12:39:38 +0200 Subject: [PATCH 110/278] Issue #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK word. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ff6fa44d..c8307bd6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK + word. - #2272983 by idflood, drunken monkey: Fixed Highlighting processor for queries without returned results. - #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 232e652e..f094d9d4 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -22,8 +22,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { /** * PREG regular expression for splitting words. * - * We highlight around non-indexable or CJK characters. - * * @var string */ protected static $split; @@ -40,7 +38,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' . '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}'; self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']))'; - self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']+/iu'; + self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/iu'; } /** From 38ecbffcc875ba4e29e23c1ee94fb991d1cccd22 Mon Sep 17 00:00:00 2001 From: alanmackenzie Date: Wed, 9 Jul 2014 15:33:13 +0200 Subject: [PATCH 111/278] Issue #2146435 by alanmackenzie: Fixed Views paging with custom pager add-ons. --- CHANGELOG.txt | 1 + .../includes/plugin_cache.inc | 1 + contrib/search_api_views/includes/query.inc | 20 +++++++++---------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c8307bd6..44accaaf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2146435 by alanmackenzie: Fixed Views paging with custom pager add-ons. - #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK word. - #2272983 by idflood, drunken monkey: Fixed Highlighting processor for queries diff --git a/contrib/search_api_views/includes/plugin_cache.inc b/contrib/search_api_views/includes/plugin_cache.inc index 52724a00..6676d87c 100644 --- a/contrib/search_api_views/includes/plugin_cache.inc +++ b/contrib/search_api_views/includes/plugin_cache.inc @@ -93,6 +93,7 @@ class SearchApiViewsCache extends views_plugin_cache_time { 'super-user' => $user->uid == 1, // special caching for super user. 'language' => $GLOBALS['language']->language, 'base_url' => $GLOBALS['base_url'], + 'current_page' => $this->view->get_current_page(), ); // Not sure what gets passed in exposed_info, so better include it. All // other parameters used in the parent method are already reflected in the diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index d84a1ad2..41af8054 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -247,16 +247,6 @@ class SearchApiViewsQuery extends views_plugin_query { $view->init_pager(); $this->pager->query(); - // Views passes sometimes NULL and sometimes the integer 0 for "All" in a - // pager. If set to 0 items, a string "0" is passed. Therefore, we unset - // the limit if an empty value OTHER than a string "0" was passed. - if (!$this->limit && $this->limit !== '0') { - $this->limit = NULL; - } - // Set the range. (We always set this, as there might even be an offset if - // all items are shown.) - $this->query->range($this->offset, $this->limit); - // Set the search ID, if it was not already set. if ($this->query->getOption('search id') == get_class($this->query)) { $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display); @@ -314,6 +304,16 @@ class SearchApiViewsQuery extends views_plugin_query { // Trigger pager pre_execute(). $this->pager->pre_execute($this->query); + // Views passes sometimes NULL and sometimes the integer 0 for "All" in a + // pager. If set to 0 items, a string "0" is passed. Therefore, we unset + // the limit if an empty value OTHER than a string "0" was passed. + if (!$this->limit && $this->limit !== '0') { + $this->limit = NULL; + } + // Set the range. (We always set this, as there might even be an offset if + // all items are shown.) + $this->query->range($this->offset, $this->limit); + $start = microtime(TRUE); // Execute the search. From f87546af1b4a96ed07d716e0cd32a603ac4e6d23 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 10 Jul 2014 10:10:00 +0200 Subject: [PATCH 112/278] Fixed signature of the SearchApiViewsQuery::getOption() proxy method. --- contrib/search_api_views/includes/query.inc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 41af8054..712a5e2d 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -660,16 +660,18 @@ class SearchApiViewsQuery extends views_plugin_query { return $ret; } - public function getOption($name) { + public function getOption($name, $default = NULL) { if (!$this->errors) { - return $this->query->getOption($name); + return $this->query->getOption($name, $default); } + return $default; } public function setOption($name, $value) { if (!$this->errors) { return $this->query->setOption($name, $value); } + return NULL; } public function &getOptions() { From c7f2f3ef1b0ee3735807327cfc904fccc13d7fd3 Mon Sep 17 00:00:00 2001 From: aaronbauman Date: Sun, 20 Jul 2014 11:26:04 +0200 Subject: [PATCH 113/278] Issue #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) in Views. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/handler_sort.inc | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 44accaaf..875260ec 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) in Views. - #2146435 by alanmackenzie: Fixed Views paging with custom pager add-ons. - #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK word. diff --git a/contrib/search_api_views/includes/handler_sort.inc b/contrib/search_api_views/includes/handler_sort.inc index 1b2cb195..a6aa6a98 100644 --- a/contrib/search_api_views/includes/handler_sort.inc +++ b/contrib/search_api_views/includes/handler_sort.inc @@ -29,6 +29,15 @@ class SearchApiViewsHandlerSort extends views_handler_sort { $sort = &$this->query->getSort(); $sort = array(); } + + // If two of the same fields are used for sort, ignore the latter in order + // for the prior to take precedence. (Temporary workaround until + // https://www.drupal.org/node/2145547 is fixed in Views.) + $alreadySorted = $this->query->getSort(); + if (is_array($alreadySorted) && isset($alreadySorted[$this->real_field])) { + return; + } + $this->query->sort($this->real_field, $this->options['order']); } From 8f2d3a167179630e16e15b5ad78cc445bef93ccd Mon Sep 17 00:00:00 2001 From: areynolds Date: Mon, 21 Jul 2014 18:20:11 +0200 Subject: [PATCH 114/278] Issue #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache plugins. --- CHANGELOG.txt | 2 ++ .../includes/plugin_cache.inc | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 875260ec..f247c356 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache + plugins. - #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) in Views. - #2146435 by alanmackenzie: Fixed Views paging with custom pager add-ons. - #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK diff --git a/contrib/search_api_views/includes/plugin_cache.inc b/contrib/search_api_views/includes/plugin_cache.inc index 6676d87c..be37770e 100644 --- a/contrib/search_api_views/includes/plugin_cache.inc +++ b/contrib/search_api_views/includes/plugin_cache.inc @@ -77,17 +77,18 @@ class SearchApiViewsCache extends views_plugin_cache_time { } /** - * Overrides views_plugin_cache::get_results_key(). + * Overrides views_plugin_cache::get_cache_key(). * - * Use the Search API query as the main source for the key. + * Use the Search API query as the main source for the key. Note that in + * Views < 3.8, this function does not exist. */ - public function get_results_key() { + public function get_cache_key($key_data = array()) { global $user; if (!isset($this->_results_key)) { $query = $this->getSearchApiQuery(); $query->preExecute(); - $key_data = array( + $key_data += array( 'query' => $query, 'roles' => array_keys($user->roles), 'super-user' => $user->uid == 1, // special caching for super user. @@ -101,8 +102,19 @@ class SearchApiViewsCache extends views_plugin_cache_time { if (isset($_GET['exposed_info'])) { $key_data['exposed_info'] = $_GET['exposed_info']; } + } + $key = md5(serialize($key_data)); + return $key; + } - $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data)); + /** + * Overrides views_plugin_cache::get_results_key(). + * + * This is unnecessary for Views >= 3.8. + */ + public function get_results_key() { + if (!isset($this->_results_key)) { + $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . $this->get_cache_key(); } return $this->_results_key; From 717d2fbead6c1560599907ee5a5899467085ff42 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 23 Jul 2014 10:33:42 +0200 Subject: [PATCH 115/278] Adapted CHANGELOG.txt to 1.13 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f247c356..84ebb263 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.13 (07/23/2014): +----------------------------- - #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache plugins. - #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) in Views. From c705d48ec2c0f974c34423e244388eb44d729d0a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 23 Jul 2014 10:39:35 +0200 Subject: [PATCH 116/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 84ebb263..29d2ccfa 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.13 (07/23/2014): ----------------------------- - #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache From b28315000294d118f2fa8c1547ae58e24026cb07 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 19 Aug 2014 10:41:39 +0200 Subject: [PATCH 117/278] Issue #2278737 by drunken monkey: Fixed use of multiple Views fulltext search filters. --- CHANGELOG.txt | 2 + .../includes/handler_filter_fulltext.inc | 43 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 29d2ccfa..90b936ac 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2278737 by drunken monkey: Fixed use of multiple Views fulltext search + filters. Search API 1.13 (07/23/2014): ----------------------------- diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index 6b5cba12..b1208bd3 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -171,7 +171,8 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex } $this->query->fields($fields); - $old = $this->query->getOriginalKeys(); + $old = $this->query->getKeys(); + $old_original = $this->query->getOriginalKeys(); $this->query->keys($this->value); if ($this->operator == 'NOT') { $keys = &$this->query->getKeys(); @@ -182,16 +183,44 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex // We can't know how negation is expressed in the server's syntax. } } + + // If there were fulltext keys set, we take care to combine them in a + // meaningful way (especially with negated keys). if ($old) { $keys = &$this->query->getKeys(); + // Array-valued keys are combined. if (is_array($keys)) { - $keys[] = $old; - } - elseif (is_array($old)) { - // We don't support such nonsense. + // If the old keys weren't parsed into an array, we instead have to + // combine the original keys. + if (is_scalar($old)) { + $keys = "($old) ({$this->value})"; + } + else { + // If the conjunction or negation settings aren't the same, we have to + // nest both old and new keys array. + if (!empty($keys['#negation']) != !empty($old['#negation']) || $keys['#conjunction'] != $old['#conjunction']) { + $keys = array( + '#conjunction' => 'AND', + $old, + $keys, + ); + } + // Otherwise, just add all individual words from the old keys to the + // new ones. + else { + foreach (element_children($old) as $i) { + $keys[] = $old[$i]; + } + } + } } - else { - $keys = "($old) ($keys)"; + // If the parse mode was "direct" for both old and new keys, we + // concatenate them and set them both via method and reference (to also + // update the originalKeys property. + elseif (is_scalar($old_original)) { + $combined_keys = "($old_original) ($keys)"; + $this->query->keys($combined_keys); + $keys = $combined_keys; } } } From 2feb041869310ed6dae5f659f26efd41a5dd6165 Mon Sep 17 00:00:00 2001 From: solotandem Date: Wed, 27 Aug 2014 14:37:12 +0200 Subject: [PATCH 118/278] Issue #2319263 by solotandem: Added easier way to subclass entity classes. --- CHANGELOG.txt | 1 + includes/index_entity.inc | 4 ++-- includes/server_entity.inc | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 90b936ac..b68b402d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2319263 by solotandem: Added easier way to subclass entity classes. - #2278737 by drunken monkey: Fixed use of multiple Views fulltext search filters. diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 745e9049..db0116c1 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -172,8 +172,8 @@ class SearchApiIndex extends Entity { /** * Constructor as a helper to the parent constructor. */ - public function __construct(array $values = array()) { - parent::__construct($values, 'search_api_index'); + public function __construct(array $values = array(), $entity_type = 'search_api_index') { + parent::__construct($values, $entity_type); } /** diff --git a/includes/server_entity.inc b/includes/server_entity.inc index be2a568c..4a991166 100644 --- a/includes/server_entity.inc +++ b/includes/server_entity.inc @@ -74,8 +74,8 @@ class SearchApiServer extends Entity { /** * Constructor as a helper to the parent constructor. */ - public function __construct(array $values = array()) { - parent::__construct($values, 'search_api_server'); + public function __construct(array $values = array(), $entity_type = 'search_api_server') { + parent::__construct($values, $entity_type); } /** From cdbd1572e892601d2d512957cb07e4499652d58b Mon Sep 17 00:00:00 2001 From: ufku Date: Thu, 28 Aug 2014 15:39:49 +0200 Subject: [PATCH 119/278] Follow-up to #2110315 by ufku, drunken monkey: Fixed Views field handler for "Entity reference" fields. --- .../search_api_views.views.inc | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index b55a85a7..00f06426 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -134,8 +134,8 @@ function search_api_views_views_data() { if (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') { $field_id = ($pos = strrpos($key, ':')) ? substr($key, $pos + 1) : $key; $field_info = field_info_field($field_id); - if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) { - $vocabulary_fields[$field_info['settings']['allowed_values'][0]['vocabulary']][] = $key; + if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) { + $vocabulary_fields[$vocabulary][] = $key; } else { $vocabulary_fields[''][] = $key; @@ -209,7 +209,6 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper } elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm'; - $info = $wrapper->info(); $field_info = field_info_field($info['name']); // For the "Parent terms" and "All parent terms" properties, we can // extrapolate the vocabulary from the parent in the selector. (E.g., @@ -221,8 +220,8 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $field_info = field_info_field($parts[count($parts) - 2]); } } - if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) { - $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary']; + if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) { + $table[$id]['filter']['vocabulary'] = $vocabulary; } } else { @@ -293,3 +292,31 @@ function search_api_views_views_plugins() { return $ret; } + +/** + * Returns the vocabulary machine name of a term field. + * + * @param array|null $field_info + * The field's field info array, or NULL if the field is not provided by the + * Field API. See the return value of field_info_field(). + * + * @return string|null + * If the field contains taxonomy terms of a single vocabulary (which could be + * determined), that vocabulary's machine name; NULL otherwise. + */ +function _search_api_views_get_field_vocabulary($field_info) { + // Test for "Term reference" fields. + if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) { + return $field_info['settings']['allowed_values'][0]['vocabulary']; + } + // Test for "Entity reference" fields. + elseif (isset($field_info['settings']['handler']) && $field_info['settings']['handler'] === 'base') { + if (!empty($field_info['settings']['handler_settings']['target_bundles'])) { + $bundles = $field_info['settings']['handler_settings']['target_bundles']; + if (count($bundles) == 1) { + return key($bundles); + } + } + } + return NULL; +} From 04836364d9345d495fe00aa1e06f4c3503bc18c3 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 8 Sep 2014 14:57:30 +0200 Subject: [PATCH 120/278] Issue #2305627 by drunken monkey, cpliakas: Fixed date facets not displayed when the configured granularity is larger than the calculated granularity. --- CHANGELOG.txt | 2 ++ .../search_api_facetapi/plugins/facetapi/query_type_date.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b68b402d..d60ece86 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2305627 by drunken monkey, cpliakas: Fixed date facets not displayed when + the configured granularity is larger than the calculated granularity. - #2319263 by solotandem: Added easier way to subclass entity classes. - #2278737 by drunken monkey: Fixed use of multiple Views fulltext search filters. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index fd93de41..8a565302 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -238,7 +238,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue $timestamps = array_keys($raw_values); if (NULL === $parent) { if (count($raw_values) > 1) { - $granularity = facetapi_get_timestamp_gap(min($timestamps), max($timestamps)); + $granularity = facetapi_get_timestamp_gap(min($timestamps), max($timestamps), $max_granularity); // Array of numbers used to determine whether the next gap is smaller than // the minimum gap allowed in the drilldown. $gap_numbers = array( From ccc5ad71f9b1aa5bad97291997710009022f942b Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 8 Sep 2014 15:01:18 +0200 Subject: [PATCH 121/278] Issue #1372092 by drunken monkey: Added an error message when no service class is available when creating a server. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d60ece86..cfd33577 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1372092 by drunken monkey: Added an error message when no service class is + available when creating a server. - #2305627 by drunken monkey, cpliakas: Fixed date facets not displayed when the configured granularity is larger than the calculated granularity. - #2319263 by solotandem: Added easier way to subclass entity classes. diff --git a/search_api.admin.inc b/search_api.admin.inc index a145ec7e..cb85e563 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -252,6 +252,13 @@ function search_api_admin_add_server(array $form, array &$form_state) { $form['options']['#prefix'] = '
    '; $form['options']['#suffix'] = '
    '; + // If $info is not set, there are no service classes. Display an error message + // telling the user how to change that and return an empty form. + if (!isset($info)) { + drupal_set_message(t('There are no service classes available for the Search API. Please install a module that provides a service class to proceed.', array('@url' => url('https://www.drupal.org/node/1254698'))), 'error'); + return array(); + } + $form['submit'] = array( '#type' => 'submit', '#value' => t('Create server'), From 3271ed0c2ca20cbf9738af1a5e6124213f33019c Mon Sep 17 00:00:00 2001 From: AlexBukach Date: Mon, 15 Sep 2014 11:14:24 +0200 Subject: [PATCH 122/278] Issue #2334727 by Alex Bukach, drunken monkey: Fixed Views caching does not take items_per_page into account. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/plugin_cache.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cfd33577..7e79d060 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2334727 by Alex Bukach, drunken monkey: Fixed Views caching does not take + items_per_page into account. - #1372092 by drunken monkey: Added an error message when no service class is available when creating a server. - #2305627 by drunken monkey, cpliakas: Fixed date facets not displayed when diff --git a/contrib/search_api_views/includes/plugin_cache.inc b/contrib/search_api_views/includes/plugin_cache.inc index be37770e..5e0dff34 100644 --- a/contrib/search_api_views/includes/plugin_cache.inc +++ b/contrib/search_api_views/includes/plugin_cache.inc @@ -94,7 +94,7 @@ class SearchApiViewsCache extends views_plugin_cache_time { 'super-user' => $user->uid == 1, // special caching for super user. 'language' => $GLOBALS['language']->language, 'base_url' => $GLOBALS['base_url'], - 'current_page' => $this->view->get_current_page(), + 'offset' => $this->view->get_current_page() . '*' . $this->view->get_items_per_page() . '+' . $this->view->get_offset(), ); // Not sure what gets passed in exposed_info, so better include it. All // other parameters used in the parent method are already reflected in the From 74d16a89e235ae077bd310f5f5653213045688c6 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 17 Sep 2014 13:47:54 +0200 Subject: [PATCH 123/278] Issue #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index fields cache. --- CHANGELOG.txt | 2 ++ includes/index_entity.inc | 4 +++- search_api.module | 13 +------------ 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7e79d060..52318812 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index + fields cache. - #2334727 by Alex Bukach, drunken monkey: Fixed Views caching does not take items_per_page into account. - #1372092 by drunken monkey: Added an error message when no service class is diff --git a/includes/index_entity.inc b/includes/index_entity.inc index db0116c1..e252e1f1 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -1005,12 +1005,14 @@ class SearchApiIndex extends Entity { } /** - * Reset internal static caches. + * Reset internal caches. * * Should be used when things like fields or data alterations change to avoid * using stale data. */ public function resetCaches() { + cache_clear_all($this->getCacheId(''), 'cache', TRUE); + $this->datasource = NULL; $this->server_object = NULL; $this->callbacks = NULL; diff --git a/search_api.module b/search_api.module index b8a3cf16..79d0bc5d 100644 --- a/search_api.module +++ b/search_api.module @@ -684,28 +684,17 @@ function search_api_search_api_index_update(SearchApiIndex $index) { } // If the fields were changed, call the appropriate service class hook method - // and re-index the content, if necessary. Also, clear the fields cache. + // and re-index the content, if necessary. $old_fields = $index->original->options + array('fields' => array()); $old_fields = $old_fields['fields']; $new_fields = $index->options + array('fields' => array()); $new_fields = $new_fields['fields']; if ($old_fields != $new_fields) { - cache_clear_all($index->getCacheId(), 'cache', TRUE); if ($index->server) { $index->server()->fieldsUpdated($index); } } - // If additional fields changed, clear the index's specific cache which - // includes them. - $old_additional = $index->original->options + array('additional fields' => array()); - $old_additional = $old_additional['additional fields']; - $new_additional = $index->options + array('additional fields' => array()); - $new_additional = $new_additional['additional fields']; - if ($old_additional != $new_additional) { - cache_clear_all($index->getCacheId() . '-0-1', 'cache'); - } - // If the index's enabled or read-only status is being changed, queue or // dequeue items for indexing. if (!$index->read_only && $index->enabled != $index->original->enabled) { From 157758d2ef767bc92040b99f7f79de2384f58ee9 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 22 Sep 2014 13:21:28 +0200 Subject: [PATCH 124/278] Fixed error message in search_api_search_api_query_alter(). --- search_api.module | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/search_api.module b/search_api.module index 79d0bc5d..2ddfa6f1 100644 --- a/search_api.module +++ b/search_api.module @@ -1952,7 +1952,14 @@ function search_api_search_api_query_alter(SearchApiQueryInterface $query) { } } else { - watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $user)), WATCHDOG_WARNING); + $account = $query->getOption('search_api_access_account', '(' . t('none') . ')'); + if (is_object($account)) { + $account = $account->uid; + } + if (!is_scalar($account)) { + $account = var_export($account, TRUE); + } + watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $account), WATCHDOG_WARNING); } } } From fc4c7f51e9f333bddde96a5da8c15e53d3ae3a41 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 13 Oct 2014 12:30:41 +0200 Subject: [PATCH 125/278] Issue #2174163 by drunken monkey: Fixed detection of field type changes by data alterations. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 17 ++++------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 52318812..9b0073ee 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2174163 by drunken monkey: Fixed detection of field type changes by data + alterations. - #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index fields cache. - #2334727 by Alex Bukach, drunken monkey: Fixed Views caching does not take diff --git a/search_api.admin.inc b/search_api.admin.inc index cb85e563..0654f611 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1603,6 +1603,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) foreach ($form_state['callbacks'] as $name => $callback) { // Check whether callback status has changed. if ($values['callbacks'][$name]['status'] == empty($options['data_alter_callbacks'][$name]['status'])) { + $callbacks_changed = TRUE; if ($values['callbacks'][$name]['status']) { // Callback was just enabled, add its fields. $properties = $callback->propertyInfo(); @@ -1629,16 +1630,6 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) } } } - else { - // Callback was just disabled, remove its fields. - $properties = $callback->propertyInfo(); - if ($properties) { - foreach ($properties as $key => $field) { - unset($index->options['fields'][$key]); - } - } - - } } } @@ -1652,9 +1643,9 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) uasort($index->options['data_alter_callbacks'], 'search_api_admin_element_compare'); uasort($index->options['processors'], 'search_api_admin_element_compare'); - // Reset the index's internal property cache to correctly incorporate the - // new data alterations. - $index->resetCaches(); + // Re-calculate the fields, since they might have changed in hard-to-predict + // ways. + search_api_index_recalculate_fields(array($index)); $index->save(); $index->reindex(); From 519172a2e35d5eb7b7c4cc4561565b9e8bd17d64 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 30 Oct 2014 09:34:16 +0100 Subject: [PATCH 126/278] Issue #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. --- CHANGELOG.txt | 2 + includes/callback_bundle_filter.inc | 45 +++-- includes/datasource.inc | 74 +++++++- includes/datasource_entity.inc | 197 +++++++++++++++++++--- includes/index_entity.inc | 5 +- search_api.admin.inc | 251 ++++++++++++++++++++-------- search_api.module | 69 +++++++- search_api.test | 73 ++++++++ 8 files changed, 601 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9b0073ee..0f241547 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1184610 by drunken monkey: Added option to limit indexes to specific entity + bundles. - #2174163 by drunken monkey: Fixed detection of field type changes by data alterations. - #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index 576fa608..08fcba45 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -14,7 +14,9 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * {@inheritdoc} */ public function supportsIndex(SearchApiIndex $index) { - return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); + $support = $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); + $support &= empty($index->options['datasource']['bundles']) || count($index->options['datasource']['bundles']) > 1; + return $support; } /** @@ -44,25 +46,32 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { foreach ($info['bundles'] as $bundle => $bundle_info) { $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; } - $form = array( - 'default' => array( - '#type' => 'radios', - '#title' => t('Which items should be indexed?'), - '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, - '#options' => array( - 1 => t('All but those from one of the selected bundles'), - 0 => t('Only those from the selected bundles'), - ), - ), - 'bundles' => array( - '#type' => 'select', - '#title' => t('Bundles'), - '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), - '#options' => $options, - '#size' => min(4, count($options)), - '#multiple' => TRUE, + if (!empty($this->index->options['datasource']['bundles'])) { + $form['message']['#markup'] = '

    ' . t("Note: This index is already restricted to certain bundles. If you use this data alteration, those will be reduced further. However, the index setting is better supported in the user interface and should therefore be prefered. For example, using this data alteration will not reduce the displayed total number of items to index (even though some of them will not be indexed). Consider creating a new index with appropriate bundle settings instead.") . '

    '; + $included_bundles = drupal_map_assoc($this->index->options['datasource']['bundles']); + foreach ($options as $bundle => $label) { + if (!isset($included_bundles[$bundle])) { + unset($options[$bundle]); + } + } + } + $form['default'] = array( + '#type' => 'radios', + '#title' => t('Which items should be indexed?'), + '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, + '#options' => array( + 1 => t('All but those from one of the selected bundles'), + 0 => t('Only those from the selected bundles'), ), ); + $form['bundles'] = array( + '#type' => 'select', + '#title' => t('Bundles'), + '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), + '#options' => $options, + '#size' => min(4, count($options)), + '#multiple' => TRUE, + ); } else { $form = array( diff --git a/includes/datasource.inc b/includes/datasource.inc index cf0507fd..bdab0293 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -281,6 +281,56 @@ interface SearchApiDataSourceControllerInterface { */ public function getEntityType(); + /** + * Form constructor for configuring the datasource for a given index. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $form_state + * The form state. $form_state['index'] will contain the edited index. If + * this key is empty, then a new index is being created. In case of an edit, + * $form_state['index']->options['datasource'] contains the previous + * settings for the datasource. + * + * @return array|false + * A form array for configuring this callback, or FALSE if no configuration + * is possible. + */ + public function configurationForm(array $form, array &$form_state); + + /** + * Validation callback for the form returned by configurationForm(). + * + * This method will only be called if that form was non-empty. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $values + * The part of the $form_state['values'] array corresponding to this form. + * @param array $form_state + * The complete form state. + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state); + + /** + * Submit callback for the form returned by configurationForm(). + * + * This method will only be called if that form was non-empty. + * + * Any necessary changes to the submitted values should be made, afterwards + * they will automatically be stored as the index's "datasource" options. The + * method can also be used by the datasource controller to react to the + * possible change in its settings. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $values + * The part of the $form_state['values'] array corresponding to this form. + * @param array $form_state + * The complete form state. + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state); + } /** @@ -521,6 +571,10 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return; } + foreach ($indexes as $index) { + $this->checkIndex($index); + } + // Since large amounts of items can overstrain the database, only add items // in chunks. foreach (array_chunk($item_ids, 1000) as $chunk) { @@ -528,7 +582,6 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou ->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn)); foreach ($chunk as $item_id) { foreach ($indexes as $index) { - $this->checkIndex($index); $insert->values(array( $this->itemIdColumn => $item_id, $this->indexIdColumn => $index->id, @@ -659,6 +712,25 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return array('indexed' => $indexed, 'total' => $total); } + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + } + /** * Checks whether the given index is valid for this datasource controller. * diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 836f57e3..5f3d4895 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -10,28 +10,69 @@ */ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController { + /** + * Entity type info for this type. + * + * @var array + */ + protected $entityInfo; + + /** + * The ID key of this entity type, if any. + * + * @var string|null + */ + protected $idKey; + + /** + * The bundle key of this entity type, if any. + * + * @var string|null + */ + protected $bundleKey; + + /** + * Cached return values for getBundles(), keyed by index machine name. + * + * @var array + */ + protected $bundles = array(); + + /** + * {@inheritdoc} + */ + public function __construct($type) { + parent::__construct($type); + + $this->entityInfo = entity_get_info($this->entityType); + if (!empty($this->entityInfo['entity keys']['id'])) { + $this->idKey = $this->entityInfo['entity keys']['id']; + } + if (!empty($this->entityInfo['entity keys']['bundle'])) { + $this->bundleKey = $this->entityInfo['entity keys']['bundle']; + } + } + /** * {@inheritdoc} */ public function getIdFieldInfo() { - $info = entity_get_info($this->entityType); $properties = entity_get_property_info($this->entityType); - if (empty($info['entity keys']['id'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label']))); + if (!$this->idKey) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $this->entityInfo['label']))); } - $field = $info['entity keys']['id']; - if (empty($properties['properties'][$field]['type'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field))); + if (empty($properties['properties'][$this->idKey]['type'])) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); } - $type = $properties['properties'][$field]['type']; + $type = $properties['properties'][$this->idKey]['type']; if (search_api_is_list_type($type)) { - throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field))); + throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); } if ($type == 'token') { $type = 'string'; } return array( - 'key' => $field, + 'key' => $this->idKey, 'type' => $type, ); } @@ -103,24 +144,24 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon // all items again without any key conflicts. $this->stopTracking($indexes); - $entity_info = entity_get_info($this->entityType); - - if (!empty($entity_info['base table'])) { + if (!empty($this->entityInfo['base table']) && $this->idKey) { // Use a subselect, which will probably be much faster than entity_load(). // Assumes that all entities use the "base table" property and the // "entity keys[id]" in the same way as the default controller. - $id_field = $entity_info['entity keys']['id']; - $table = $entity_info['base table']; + $table = $this->entityInfo['base table']; - // We could also use a single insert (with a JOIN in the nested query), + // We could also use a single insert (with a UNION in the nested query), // but this method will be mostly called with a single index, anyways. foreach ($indexes as $index) { // Select all entity ids. $query = db_select($table, 't'); - $query->addField('t', $id_field, 'item_id'); + $query->addField('t', $this->idKey, 'item_id'); $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id)); $query->addExpression('1', 'changed'); + if ($bundles = $this->getIndexBundles($index)) { + $query->condition($this->bundleKey, $bundles); + } // INSERT ... SELECT ... db_insert($this->table) @@ -129,16 +170,132 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } } else { - // In the absence of a 'base table', use the slow entity_load(). - parent::startTracking($indexes); + // In the absence of a 'base table', use the slower way of retrieving the + // items and inserting them "manually". For each index we get the item IDs + // (since selected bundles might differ) and insert all of them as new. + foreach ($indexes as $index) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', $this->entityType); + if ($bundles = $this->getIndexBundles($index)) { + $query->entityCondition('bundle', $bundles); + } + $result = $query->execute(); + $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array(); + if ($ids) { + $this->trackItemInsert($ids, array($index)); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function trackItemInsert(array $item_ids, array $indexes) { + foreach ($indexes as $index) { + $ids = $item_ids; + if ($bundles = $this->getIndexBundles($index)) { + $ids = drupal_map_assoc($ids); + foreach (entity_load($this->entityType, $ids) as $id => $entity) { + if (empty($bundles[$entity->{$this->bundleKey}])) { + unset($ids[$id]); + } + } + } + if ($ids) { + parent::trackItemInsert($ids, array($index)); + } + } + } + + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + // We don't allow later editing of the included bundles. + if (!empty($form_state['index'])) { + return FALSE; } + + $options = $this->getAvailableBundles(); + if (!$options) { + return; + } + $form['bundles'] = array( + '#type' => 'checkboxes', + '#title' => t('Bundles'), + '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed afterwards.'), + '#options' => $options, + '#attributes' => array('class' => array('search-api-checkboxes-list')), + ); + return $form; } /** * {@inheritdoc} */ - protected function getAllItemIds() { - return array_keys(entity_load($this->entityType)); + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + if (!empty($values['bundles'])) { + $values['bundles'] = array_keys(array_filter($values['bundles'])); + } + } + + /** + * Retrieves the available bundles for this entity type. + * + * @return array + * An array (which might be empty) mapping this entity type's bundle keys to + * their labels. + */ + protected function getAvailableBundles() { + if (!$this->bundleKey || empty($this->entityInfo['bundles'])) { + return array(); + } + $bundles = array(); + foreach ($this->entityInfo['bundles'] as $bundle => $bundle_info) { + $bundles[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; + } + return $bundles; + } + + /** + * Computes the bundles that should be indexed for an index. + * + * @param SearchApiIndex $index + * The index for which to check. + * + * @return array + * An array containing all bundles that should be included in this index, as + * both the keys and values. An empty array means all current bundles should + * be included. + * + * @throws SearchApiException + * If the index doesn't belong to this datasource controller. + */ + protected function getIndexBundles(SearchApiIndex $index) { + $this->checkIndex($index); + + if (!isset($this->bundles[$index->machine_name])) { + $this->bundles[$index->machine_name] = array(); + if (!empty($index->options['datasource']['bundles'])) { + // We retrieve the available bundles here to check whether all of them + // are included by the index's setting. In this case, we return an empty + // array, too, to save on complexity. + // On the other hand, we still want to return deleted bundles since we + // do not want to suddenly include all bundles when all selected bundles + // were deleted. + $available = $this->getAvailableBundles(); + foreach ($index->options['datasource']['bundles'] as $bundle) { + $this->bundles[$index->machine_name][$bundle] = $bundle; + unset($available[$bundle]); + } + if (!$available) { + $this->bundles[$index->machine_name] = array(); + } + } + } + + return $this->bundles[$index->machine_name]; } } diff --git a/includes/index_entity.inc b/includes/index_entity.inc index e252e1f1..9fc1f68b 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -115,7 +115,8 @@ class SearchApiIndex extends Entity { public $item_type; /** - * An array of options for configuring this index. The layout is as follows: + * An array of options for configuring this index. The layout is as follows + * (with all keys being optional): * - cron_limit: The maximum number of items to be indexed per cron batch. * - index_directly: Boolean setting whether entities are indexed immediately * after they are created or updated. @@ -150,6 +151,8 @@ class SearchApiIndex extends Entity { * - weight: Used for sorting the processors. * - settings: Processor-specific settings, configured via the processor's * configuration form. + * - datasource: Datasource-specific settings, configured via the datasource's + * configuration form. * * @var array */ diff --git a/search_api.admin.inc b/search_api.admin.inc index 0654f611..71cfafc1 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -682,83 +682,126 @@ function search_api_admin_form_delete_submit($form, &$form_state) { * * @ingroup forms * + * @see search_api_admin_add_index_ajax_callback() * @see search_api_admin_add_index_validate() * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index(array $form, array &$form_state) { drupal_set_title(t('Add index')); + $old_type = empty($form_state['values']['item_type']) ? '' : $form_state['values']['item_type']; + $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; $form['#tree'] = TRUE; - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Index name'), - '#maxlength' => 50, - '#required' => TRUE, - ); - $form['machine_name'] = array( - '#type' => 'machine_name', - '#maxlength' => 50, - '#machine_name' => array( - 'exists' => 'search_api_index_load', - ), - ); + if (empty($form_state['step_one'])) { + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Index name'), + '#maxlength' => 50, + '#required' => TRUE, + ); - $form['item_type'] = array( - '#type' => 'select', - '#title' => t('Item type'), - '#description' => t('Select the type of items that will be indexed in this index. ' . - 'This setting cannot be changed afterwards.'), - '#options' => array(), - '#required' => TRUE, - ); - foreach (search_api_get_item_type_info() as $type => $info) { - $form['item_type']['#options'][$type] = $info['name']; + $form['machine_name'] = array( + '#type' => 'machine_name', + '#maxlength' => 50, + '#machine_name' => array( + 'exists' => 'search_api_index_load', + ), + ); + + $form['item_type'] = array( + '#type' => 'select', + '#title' => t('Item type'), + '#description' => t('Select the type of items that will be indexed in this index. ' . + 'This setting cannot be changed afterwards.'), + '#options' => array(), + '#required' => TRUE, + '#ajax' => array( + 'callback' => 'search_api_admin_add_index_ajax_callback', + 'wrapper' => 'search-api-datasource-options', + ), + ); + $form['datasource'] = array(); + foreach (search_api_get_item_type_info() as $type => $info) { + $form['item_type']['#options'][$type] = $info['name']; + } + $form['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#description' => t('This will only take effect if you also select a server for the index.'), + '#default_value' => TRUE, + ); + $form['description'] = array( + '#type' => 'textarea', + '#title' => t('Index description'), + ); + $form['server'] = array( + '#type' => 'select', + '#title' => t('Server'), + '#description' => t('Select the server this index should reside on.'), + '#default_value' => '', + '#options' => array('' => t('< No server >')) + ); + $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); + // List enabled servers first. + foreach ($servers as $server) { + if ($server->enabled) { + $form['server']['#options'][$server->machine_name] = $server->name; + } + } + foreach ($servers as $server) { + if (!$server->enabled) { + $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); + } + } + $form['read_only'] = array( + '#type' => 'checkbox', + '#title' => t('Read only'), + '#description' => t('Do not write to this index or track the status of items in this index.'), + '#default_value' => FALSE, + ); + $form['options']['index_directly'] = array( + '#type' => 'checkbox', + '#title' => t('Index items immediately'), + '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . + 'This might have serious performance drawbacks and is generally not advised for larger sites.'), + '#default_value' => FALSE, + ); + $form['options']['cron_limit'] = array( + '#type' => 'textfield', + '#title' => t('Cron batch size'), + '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . + '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), + '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, + '#size' => 4, + '#attributes' => array('class' => array('search-api-cron-limit')), + '#element_validate' => array('element_validate_integer'), + ); } - $form['enabled'] = array( - '#type' => 'checkbox', - '#title' => t('Enabled'), - '#description' => t('This will only take effect if the selected server is also enabled.'), - '#default_value' => TRUE, - ); - $form['description'] = array( - '#type' => 'textarea', - '#title' => t('Index description'), - ); - $form['server'] = array( - '#type' => 'select', - '#title' => t('Server'), - '#description' => t('Select the server this index should reside on.'), - '#default_value' => '', - '#options' => array('' => t('< No server >')) - ); - $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); - // List enabled servers first. - foreach ($servers as $server) { - $form['server']['#options'][$server->machine_name] = $server->name; + elseif (!$old_type) { + $old_type = $form_state['step_one']['item_type']; } - $form['read_only'] = array( - '#type' => 'checkbox', - '#title' => t('Read only'), - '#description' => t('Do not write to this index or track the status of items in this index.'), - '#default_value' => FALSE, - ); - $form['options']['index_directly'] = array( - '#type' => 'checkbox', - '#title' => t('Index items immediately'), - '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . - 'This might have serious performance drawbacks and is generally not advised for larger sites.'), - '#default_value' => FALSE, + + if ($old_type) { + $datasource = search_api_get_datasource_controller($old_type); + $datasource_form = array(); + $datasource_form = $datasource->configurationForm($datasource_form, $form_state); + if ($datasource_form) { + $form['datasource'] = $datasource_form; + $form['datasource']['#parents'] = array('options', 'datasource'); + } + } + $form['datasource']['#prefix'] = '
    '; + $form['datasource']['#suffix'] = '
    '; + + $form['old_type'] = array( + '#type' => 'value', + '#value' => $old_type, ); - $form['options']['cron_limit'] = array( - '#type' => 'textfield', - '#title' => t('Cron batch size'), - '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . - '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), - '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, - '#size' => 4, - '#attributes' => array('class' => array('search-api-cron-limit')), + $form['datasource_config'] = array( + '#type' => 'value', + '#value' => !empty($datasource_form), ); $form['submit'] = array( @@ -769,22 +812,33 @@ function search_api_admin_add_index(array $form, array &$form_state) { return $form; } +/** + * AJAX submit callback for search_api_admin_add_index(). + * + * Used for displaying the matching datasource configuration form for the + * selected item type. + */ +function search_api_admin_add_index_ajax_callback(array $form, array &$form_state) { + return $form['datasource']; +} + /** * Form validation handler for search_api_admin_add_index(). * * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index_validate(array $form, array &$form_state) { - $name = $form_state['values']['machine_name']; + $values = $form_state['values']; + $name = $values['machine_name']; if (is_numeric($name)) { form_set_error('machine_name', t('The machine name must not be a pure number.')); } - $cron_limit = $form_state['values']['options']['cron_limit']; - if ($cron_limit != '' . ((int) $cron_limit)) { - // We don't enforce stricter rules and treat all negative values as -1. - form_set_error('options[cron_limit]', t('The cron batch size must be an integer.')); + if (!$values['datasource_config'] || empty($values['item_type']) || $values['item_type'] != $values['old_type']) { + return; } + $datasource = search_api_get_datasource_controller($values['item_type']); + $datasource->configurationFormValidate($form['datasource'], $form_state['values']['options']['datasource'], $form_state); } /** @@ -794,10 +848,34 @@ function search_api_admin_add_index_validate(array $form, array &$form_state) { */ function search_api_admin_add_index_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - $values = $form_state['values']; - // Validation of whether the server of an index is enabled is done in the + if (!empty($form_state['step_one'])) { + $values += $form_state['step_one']; + unset($form_state['step_one']); + } + + // The type was changed (or the form submitted without JS for the first time). + // If the new type has a configuration form, we have to display it now. + $datasource = search_api_get_datasource_controller($values['item_type']); + if ($values['item_type'] != $values['old_type']) { + $datasource_form = array(); + if ($datasource->configurationForm($datasource_form, $form_state)) { + unset($values['options']['datasource']); + $form_state['step_one'] = $values; + $form_state['rebuild'] = TRUE; + drupal_set_message(t('Please specify further configuration options.')); + return; + } + } + + // If the current type has a configuration form, call the datasource + // controller's config submit callback. + if ($values['datasource_config']) { + $datasource->configurationFormSubmit($form['datasource'], $values['options']['datasource'], $form_state); + } + + // Validation of whether a server is set for the index is done in the // SearchApiIndex::save() method. search_api_index_insert($values); @@ -1193,6 +1271,7 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta * * @ingroup forms * + * @see search_api_admin_index_edit_validate() * @see search_api_admin_index_edit_submit() */ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) { @@ -1208,7 +1287,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#required' => TRUE, ); try { - $enabled_fixed = !$index->enabled && !$index->server(); + $enabled_fixed = !$index->server(); } catch (Exception $e) { watchdog_exception('search_api', $e); @@ -1220,7 +1299,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#type' => 'checkbox', '#title' => t('Enabled'), '#default_value' => $index->enabled, - // Can't enable an index lying on a disabled server, or no server at all. + // Can't enable an index that's not lying on any server. '#disabled' => $enabled_fixed, ); $form['description'] = array( @@ -1246,6 +1325,13 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#description' => t('Do not write to this index or track the status of items in this index.'), '#default_value' => $index->read_only, ); + + $datasource_form = !empty($form['options']['datasource']) ? $form['options']['datasource'] : array(); + $datasource_form = $index->datasource()->configurationForm($datasource_form, $form_state); + if ($datasource_form) { + $form['options']['datasource'] = $datasource_form; + } + $form['options']['index_directly'] = array( '#type' => 'checkbox', '#title' => t('Index items immediately'), @@ -1285,14 +1371,31 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI return $form; } +/** + * Form validation handler for search_api_admin_index_edit(). + * + * @see search_api_admin_index_edit_submit() + */ +function search_api_admin_index_edit_validate(array $form, array &$form_state) { + if (!empty($form['options']['datasource'])) { + $form_state['index']->datasource()->configurationFormValidate($form['options']['datasource'], $form_state['values']['options']['datasource'], $form_state); + } +} + /** * Form submission handler for search_api_admin_index_edit(). + * + * @see search_api_admin_index_edit_validate() */ function search_api_admin_index_edit_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - $values = $form_state['values']; $index = $form_state['index']; + + if (!empty($form['options']['datasource'])) { + $index->datasource()->configurationFormSubmit($form['datasource'], $values['options']['datasource'], $form_state); + } + $values['options'] += $index->options; $ret = $index->update($values); diff --git a/search_api.module b/search_api.module index 2ddfa6f1..72047648 100644 --- a/search_api.module +++ b/search_api.module @@ -866,6 +866,8 @@ function search_api_entity_insert($entity, $type) { * datasource controller and calls search_api_track_item_change() for the * updated items. * + * It also checks whether the entity's bundle changed and acts accordingly. + * * @see search_api_search_api_item_type_info() */ function search_api_entity_update($entity, $type) { @@ -874,7 +876,16 @@ function search_api_entity_update($entity, $type) { if (!entity_get_property_info($type)) { return; } - list($id) = entity_extract_ids($type, $entity); + list($id, , $new_bundle) = entity_extract_ids($type, $entity); + + // Check if the entity's bundle changed. + if (isset($entity->original)) { + list(, , $old_bundle) = entity_extract_ids($type, $entity->original); + if ($new_bundle != $old_bundle) { + _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle); + } + } + if (isset($id)) { search_api_track_item_change($type, array($id)); } @@ -901,6 +912,26 @@ function search_api_entity_delete($entity, $type) { } } +/** + * Implements hook_field_attach_rename_bundle(). + * + * This is implemented on behalf of the SearchApiEntityDataSourceController + * datasource controller, to update any bundle settings that contain the changed + * bundle. + */ +function search_api_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { + foreach (search_api_index_load_multiple(FALSE, array('item_type' => $entity_type)) as $index) { + $bundles = &$index->options['datasource']['bundles']; + if (isset($bundles) && ($pos = array_search($bundle_old, $bundles)) !== FALSE) { + $bundles[$pos] = $bundle_new; + $index->save(); + // Clear all caches that could contain the bundle information. + $index->resetCaches(); + drupal_static_reset('search_api_get_datasource_controller'); + } + } +} + /** * Implements hook_field_update_field(). * @@ -2890,6 +2921,42 @@ function _search_api_deep_copy(array $array) { return $copy; } +/** + * Reacts to a change in the bundle of an entity. + * + * Used as a helper function in search_api_entity_update(). + * + * @param $type + * The entity's type. + * @param $id + * The entity's ID. + * @param $old_bundle + * The entity's previous bundle. + * @param $new_bundle + * The entity's new bundle. + */ +function _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle) { + $controller = search_api_get_datasource_controller($type); + $conditions = array( + 'enabled' => 1, + 'item_type' => $type, + 'read_only' => 0, + ); + foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { + if (!empty($index->options['datasource']['bundles'])) { + $bundles = drupal_map_assoc($index->options['datasource']['bundles']); + if (empty($bundles[$new_bundle]) != empty($bundles[$old_bundle])) { + if (empty($bundles[$new_bundle])) { + $controller->trackItemDelete(array($id), array($index)); + } + else { + $controller->trackItemInsert(array($id), array($index)); + } + } + } + } +} + /** * Creates and sets a batch for indexing items. * diff --git a/search_api.test b/search_api.test index 47479812..89883028 100644 --- a/search_api.test +++ b/search_api.test @@ -830,6 +830,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->index = entity_create('search_api_index', array( 'id' => 1, 'name' => 'test', + 'machine_name' => 'test', 'enabled' => 1, 'item_type' => 'user', 'options' => array( @@ -859,6 +860,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->checkIgnoreCaseProcessor(); $this->checkTokenizer(); $this->checkHtmlFilter(); + $this->checkEntityDatasource(); } /** @@ -1122,4 +1124,75 @@ END; $this->assertEqual($tmp[1]['name']['value'], $processed1, 'Text was correctly processed.'); } + /** + * Tests the entity datasource controller and its bundle setting. + */ + protected function checkEntityDatasource() { + // First, create the necessary content types. + $type = (object) array( + 'type' => 'article', + 'base' => 'article', + ); + node_type_save($type); + $type->type = $type->base = 'page'; + node_type_save($type); + + // Now, create some nodes. + $node = (object) array( + 'title' => 'Foo', + 'type' => 'article', + ); + node_save($node); + $nid1 = $node->nid; + $node = (object) array( + 'title' => 'Bar', + 'type' => 'article', + ); + node_save($node); + $node = (object) array( + 'title' => 'Baz', + 'type' => 'page', + ); + node_save($node); + + // We can't use $this->index here, since users don't have bundles. + $index = entity_create('search_api_index', array( + 'id' => 2, + 'name' => 'test2', + 'machine_name' => 'test2', + 'enabled' => 1, + 'item_type' => 'node', + 'options' => array( + 'fields' => array( + 'nid' => array( + 'type' => 'integer', + ), + ), + ), + )); + + // Now start tracking and check whether the index status is correct. + $datasource = search_api_get_datasource_controller('node'); + $datasource->startTracking(array($index)); + $status = $datasource->getIndexStatus($index); + $this->assertEqual($status['total'], 3, 'Correct number of items marked for indexing on not bundle-specific index.'); + $datasource->stopTracking(array($index)); + + // Once again, but with only indexing articles. + $index->options['datasource']['bundles'] = array('article'); + drupal_static_reset('search_api_get_datasource_controller'); + $datasource = search_api_get_datasource_controller('node'); + $datasource->startTracking(array($index)); + $status = $datasource->getIndexStatus($index); + $this->assertEqual($status['total'], 2, 'Correct number of items marked for indexing on bundle-specific index.'); + $datasource->stopTracking(array($index)); + + // Now test that bundle renaming works. + $index->save(); + field_attach_rename_bundle('node', 'article', 'foo'); + $index = search_api_index_load('test2', TRUE); + $this->assertEqual($index->options['datasource']['bundles'], array('foo'), 'Bundle was correctly renamed in index settings.'); + $index->delete(); + } + } From 88f06a8f6395ccf37b9a21cbaf2c9a2281854247 Mon Sep 17 00:00:00 2001 From: Xano Date: Wed, 12 Nov 2014 14:33:10 +0100 Subject: [PATCH 127/278] Issue #2364875 by Xano: Fixed Views argument handler for fulltext fields. --- CHANGELOG.txt | 1 + contrib/search_api_views/search_api_views.views.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0f241547..5be6c02a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2364875 by Xano: Fixed Views argument handler for fulltext fields. - #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. - #2174163 by drunken monkey: Fixed detection of field type changes by data diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index 00f06426..a87e5bcc 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -184,7 +184,7 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper if ($inner_type == 'text') { $table[$id] += array( 'argument' => array( - 'handler' => 'SearchApiViewsHandlerArgument', + 'handler' => 'SearchApiViewsHandlerArgumentString', ), 'filter' => array( 'handler' => 'SearchApiViewsHandlerFilterText', From 926c2a0c2bef841319456a633fce93c2b0da563b Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 13 Nov 2014 14:25:03 +0100 Subject: [PATCH 128/278] Issue #2364247 by drunken monkey: Fixed documentation for SearchApiQueryFilterInterface::getFilters(). --- CHANGELOG.txt | 2 ++ includes/query.inc | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5be6c02a..09c22eec 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2364247 by drunken monkey: Fixed documentation for + SearchApiQueryFilterInterface::getFilters(). - #2364875 by Xano: Fixed Views argument handler for fulltext fields. - #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. diff --git a/includes/query.inc b/includes/query.inc index 8c63bebb..7f13f16d 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -915,8 +915,11 @@ interface SearchApiQueryFilterInterface { * Return all conditions and nested filters contained in this filter. * * @return array - * An array containing this filter's subfilters. Each of these is either an - * array (field, value, operator), or another SearchApiFilter object. + * An array containing this filter's subfilters. Each of these is either a + * condition, represented as a numerically indexed array with the arguments + * of a previous SearchApiQueryFilterInterface::condition() call (field, + * value, operator); or a nested filter, represented by a + * SearchApiQueryFilterInterface filter object. */ public function &getFilters(); From 20ed1e4717d8906c408ef773ad7a641298c06610 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 13 Nov 2014 14:28:06 +0100 Subject: [PATCH 129/278] Issue #2359201 by drunken monkey: Added a "List" option to "Aggregated fields". --- CHANGELOG.txt | 1 + includes/callback_add_aggregation.inc | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 09c22eec..5c3fe4ab 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2359201 by drunken monkey: Added a "List" option to "Aggregated fields". - #2364247 by drunken monkey: Fixed documentation for SearchApiQueryFilterInterface::getFilters(). - #2364875 by Xano: Fixed Views argument handler for fulltext fields. diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 31566c70..a7737de6 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -193,6 +193,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { return isset($a) ? min($a, $b) : $b; case 'first': return isset($a) ? $a : $b; + case 'list': + if (!isset($a)) { + $a = array(); + } + $a[] = $b; + return $a; } } @@ -261,6 +267,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => t('Maximum'), 'min' => t('Minimum'), 'first' => t('First'), + 'list' => t('List'), ); case 'type': return array( @@ -270,6 +277,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => 'integer', 'min' => 'integer', 'first' => 'string', + 'list' => 'list', ); case 'description': return array( @@ -279,6 +287,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => t('The Maximum aggregation computes the numerically largest contained field value.'), 'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'), 'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'), + 'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'), ); } } From 86eafb0101f41b8c33fbcf19812fb1d1c0b2071d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 13 Nov 2014 14:29:28 +0100 Subject: [PATCH 130/278] Issue #2347367 by drunken monkey, das-peter: Fixed forgotten usages of $index->item_type. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5c3fe4ab..f9067b76 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2347367 by drunken monkey, das-peter: Fixed forgotten usages of + $index->item_type. - #2359201 by drunken monkey: Added a "List" option to "Aggregated fields". - #2364247 by drunken monkey: Fixed documentation for SearchApiQueryFilterInterface::getFilters(). diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 712a5e2d..9356bbab 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -169,7 +169,7 @@ class SearchApiViewsQuery extends views_plugin_query { '#default_value' => $this->options['search_api_bypass_access'], ); - if (entity_get_info($this->index->item_type)) { + if ($this->index->getEntityType()) { $form['entity_access'] = array( '#type' => 'checkbox', '#title' => t('Additional access checks on result entities'), @@ -374,9 +374,9 @@ class SearchApiViewsQuery extends views_plugin_query { // First off, we try to gather as much field values as possible without // loading any items. foreach ($results as $id => $result) { - if (!empty($this->options['entity_access'])) { - $entity = entity_load($this->index->item_type, array($id)); - if (!entity_access('view', $this->index->item_type, $entity[$id])) { + if (!empty($this->options['entity_access']) && ($entity_type = $this->index->getEntityType())) { + $entity = entity_load($entity_type, array($id)); + if (!entity_access('view', $entity_type, $entity[$id])) { continue; } } From 55d0a0399f52d2e7ccd74c1e031a1506527529de Mon Sep 17 00:00:00 2001 From: Cyberwolf Date: Sun, 23 Nov 2014 16:04:07 +0100 Subject: [PATCH 131/278] Issue #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexing on multiple indexes with Drush. --- CHANGELOG.txt | 2 + search_api.drush.inc | 92 ++++++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f9067b76..6c0b262a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexing on multiple + indexes with Drush. - #2347367 by drunken monkey, das-peter: Fixed forgotten usages of $index->item_type. - #2359201 by drunken monkey: Added a "List" option to "Aggregated fields". diff --git a/search_api.drush.inc b/search_api.drush.inc index f04bef42..927514d0 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -297,46 +297,72 @@ function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = N if (empty($indexes)) { return; } + + $process_batch = FALSE; foreach ($indexes as $index) { - // Get the number of remaing items to index. - try { - $datasource = $index->datasource(); - } - catch (SearchApiException $e) { - drush_log($e->getMessage(), 'error'); - continue; - } - $index_status = $datasource->getIndexStatus($index); - $remaining = $index_status['total'] - $index_status['indexed']; - if ($remaining <= 0) { - drush_log(dt("The index !index is up to date.", array('!index' => $index->name)), 'ok'); - continue; + if (_drush_search_api_batch_indexing_create($index, $limit, $batch_size)) { + $process_batch = TRUE; } + } - // Get the number of items to index per batch run. - if (!isset($batch_size)) { - $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit']; - } - elseif ($batch_size <= 0) { - $batch_size = $remaining; - } + if ($process_batch) { + drush_backend_batch_process(); + } +} - // Get the number items to index. - if (!isset($limit) || !is_int($limit += 0) || $limit <= 0) { - $limit = $remaining; - } +/** + * Creates and sets a batch for indexing items for a particular index. + * + * @param SearchApiIndex $index + * The index for which items should be indexed. + * @param int $limit + * (optional) The maximum number of items to index, or NULL to index all + * items. + * @param int $batch_size + * (optional) The number of items to index per batch, or NULL to index all + * items at once. + * + * @return bool + * TRUE if batch was created, FALSE otherwise. + */ +function _drush_search_api_batch_indexing_create(SearchApiIndex $index, $limit = NULL, $batch_size = NULL) { + // Get the number of remaining items to index. + try { + $datasource = $index->datasource(); + } + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); + return FALSE; + } + $index_status = $datasource->getIndexStatus($index); + $remaining = $index_status['total'] - $index_status['indexed']; + if ($remaining <= 0) { + drush_log(dt("The index !index is up to date.", array('!index' => $index->name)), 'ok'); + return FALSE; + } - drush_log(dt("Indexing a maximum number of !limit items (!batch_size items per batch run) for the index !index.", array('!index' => $index->name, '!limit' => $limit, '!batch_size' => $batch_size)), 'ok'); + // Get the number of items to index per batch run. + if (!isset($batch_size)) { + $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit']; + } + elseif ($batch_size <= 0) { + $batch_size = $remaining; + } - // Create the batch. - if (!_search_api_batch_indexing_create($index, $batch_size, $limit, $remaining, TRUE)) { - drush_log(dt("Couldn't create a batch, please check the batch size and limit parameters."), 'error'); - } - else { - // Launch the batch process. - drush_backend_batch_process(); - } + // Get the total number of items to index. + if (!isset($limit) || !is_int($limit += 0) || $limit <= 0) { + $limit = $remaining; } + + drush_log(dt("Indexing a maximum number of !limit items (!batch_size items per batch run) for the index !index.", array('!index' => $index->name, '!limit' => $limit, '!batch_size' => $batch_size)), 'ok'); + + // Create the batch. + if (!_search_api_batch_indexing_create($index, $batch_size, $limit, $remaining, TRUE)) { + drush_log(dt("Couldn't create a batch, please check the batch size and limit parameters."), 'error'); + return FALSE; + } + + return TRUE; } /** From c058c7bf638275130115250253895f3bd2ce90d5 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 25 Nov 2014 10:23:17 +0100 Subject: [PATCH 132/278] Issue #2375447 by drunken monkey: Added clarifying comment to foreach loop in "Aggregated fields" plugin. --- includes/callback_add_aggregation.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index a7737de6..d1ed9bce 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -298,6 +298,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { public function formButtonSubmit(array $form, array &$form_state) { $button_name = $form_state['triggering_element']['#name']; if ($button_name == 'op') { + // Increment $i until the corresponding field is not set, then create the + // field with that number as suffix. for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) { } $this->options['fields']['search_api_aggregation_' . $i] = array( From b8755fe5d8745c599b4beca3017ee7683a797172 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 11 Dec 2014 18:29:48 +0100 Subject: [PATCH 133/278] Little doc cleanup. --- search_api.api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_api.api.php b/search_api.api.php index c1af2a14..550ecbc2 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -335,7 +335,7 @@ function hook_search_api_items_indexed(SearchApiIndex $index, array $item_ids) { * Lets modules alter a search query before executing it. * * @param SearchApiQueryInterface $query - * The SearchApiQueryInterface object representing the search query. + * The search query being executed. */ function hook_search_api_query_alter(SearchApiQueryInterface $query) { // Exclude entities with ID 0. (Assume the ID field is always indexed.) From 03af04ac9e038e63288ccf122d2941cf540e33e8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 15 Dec 2014 12:09:24 +0100 Subject: [PATCH 134/278] Issue #2371099 by drunken monkey: Fixed display of active "Exclude" facets. --- CHANGELOG.txt | 1 + .../plugins/facetapi/query_type_term.inc | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6c0b262a..35499122 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2371099 by drunken monkey: Fixed display of active "Exclude" facets. - #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexing on multiple indexes with Drush. - #2347367 by drunken monkey, das-peter: Fixed forgotten usages of diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index bb91c179..b2c02c37 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -148,9 +148,15 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy return array(); } $search_id = $search_ids[$facet['name']]; - $search = search_api_current_search($search_id); + list(, $results) = search_api_current_search($search_id); $build = array(); - $results = $search[1]; + + // Always include the active facet items. + foreach ($this->adapter->getActiveItems($this->facet) as $filter) { + $build[$filter['value']]['#count'] = $results['result count']; + } + + // Then, add the facets returned by the server. if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) { $values = $results['search_api_facets'][$this->facet['name']]; foreach ($values as $value) { From a20d6c7e5c613df18017bc8096ad32c16d4808f2 Mon Sep 17 00:00:00 2001 From: illusionuk Date: Tue, 16 Dec 2014 10:32:44 +0100 Subject: [PATCH 135/278] Issue #2382385 by illusionuk, drunken monkey: Fixed error handling when using invalid fulltext or sort field in Views. --- CHANGELOG.txt | 2 ++ .../includes/handler_argument_fulltext.inc | 8 +++++++- .../search_api_views/includes/handler_filter_fulltext.inc | 8 +++++++- contrib/search_api_views/includes/handler_sort.inc | 8 +++++++- includes/query.inc | 3 +++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 35499122..1043c860 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2382385 by illusionuk, drunken monkey: Fixed error handling when using + invalid fulltext or sort field in Views. - #2371099 by drunken monkey: Fixed display of active "Exclude" facets. - #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexing on multiple indexes with Drush. diff --git a/contrib/search_api_views/includes/handler_argument_fulltext.inc b/contrib/search_api_views/includes/handler_argument_fulltext.inc index a6f00e2d..a4db8e25 100644 --- a/contrib/search_api_views/includes/handler_argument_fulltext.inc +++ b/contrib/search_api_views/includes/handler_argument_fulltext.inc @@ -66,7 +66,13 @@ class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumen */ public function query($group_by = FALSE) { if ($this->options['fields']) { - $this->query->fields($this->options['fields']); + try { + $this->query->fields($this->options['fields']); + } + catch (SearchApiException $e) { + $this->query->abort($e->getMessage()); + return; + } } if ($this->options['conjunction'] != 'AND') { $this->query->setOption('conjunction', $this->options['conjunction']); diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index b1208bd3..71745aea 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -170,7 +170,13 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex $this->query->setOption('conjunction', 'OR'); } - $this->query->fields($fields); + try { + $this->query->fields($fields); + } + catch (SearchApiException $e) { + $this->query->abort($e->getMessage()); + return; + } $old = $this->query->getKeys(); $old_original = $this->query->getOriginalKeys(); $this->query->keys($this->value); diff --git a/contrib/search_api_views/includes/handler_sort.inc b/contrib/search_api_views/includes/handler_sort.inc index a6aa6a98..d7ca1e47 100644 --- a/contrib/search_api_views/includes/handler_sort.inc +++ b/contrib/search_api_views/includes/handler_sort.inc @@ -28,6 +28,7 @@ class SearchApiViewsHandlerSort extends views_handler_sort { unset($this->query->orderby); $sort = &$this->query->getSort(); $sort = array(); + unset($sort); } // If two of the same fields are used for sort, ignore the latter in order @@ -38,7 +39,12 @@ class SearchApiViewsHandlerSort extends views_handler_sort { return; } - $this->query->sort($this->real_field, $this->options['order']); + try { + $this->query->sort($this->real_field, $this->options['order']); + } + catch (SearchApiException $e) { + $this->query->abort($e->getMessage()); + } } } diff --git a/includes/query.inc b/includes/query.inc index 7f13f16d..b887c795 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -226,6 +226,9 @@ interface SearchApiQueryInterface { * * This method should always be called by execute() and contain all necessary * operations before the query is passed to the server's search() method. + * + * @throws SearchApiException + * If any error occurred during the preparation of the query. */ public function preExecute(); From 848b9c0aa771255e1c79d7fb0c08e539379563ee Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 26 Dec 2014 08:36:04 +0100 Subject: [PATCH 136/278] Revert "Issue #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles." This reverts commit 519172a2e35d5eb7b7c4cc4561565b9e8bd17d64. Conflicts: CHANGELOG.txt --- CHANGELOG.txt | 2 - includes/callback_bundle_filter.inc | 45 ++--- includes/datasource.inc | 74 +------- includes/datasource_entity.inc | 197 +++------------------- includes/index_entity.inc | 5 +- search_api.admin.inc | 251 ++++++++-------------------- search_api.module | 69 +------- search_api.test | 73 -------- 8 files changed, 115 insertions(+), 601 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1043c860..aa29fd42 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -11,8 +11,6 @@ Search API 1.x, dev (xx/xx/xxxx): - #2364247 by drunken monkey: Fixed documentation for SearchApiQueryFilterInterface::getFilters(). - #2364875 by Xano: Fixed Views argument handler for fulltext fields. -- #1184610 by drunken monkey: Added option to limit indexes to specific entity - bundles. - #2174163 by drunken monkey: Fixed detection of field type changes by data alterations. - #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index 08fcba45..576fa608 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -14,9 +14,7 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * {@inheritdoc} */ public function supportsIndex(SearchApiIndex $index) { - $support = $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); - $support &= empty($index->options['datasource']['bundles']) || count($index->options['datasource']['bundles']) > 1; - return $support; + return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); } /** @@ -46,31 +44,24 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { foreach ($info['bundles'] as $bundle => $bundle_info) { $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; } - if (!empty($this->index->options['datasource']['bundles'])) { - $form['message']['#markup'] = '

    ' . t("Note: This index is already restricted to certain bundles. If you use this data alteration, those will be reduced further. However, the index setting is better supported in the user interface and should therefore be prefered. For example, using this data alteration will not reduce the displayed total number of items to index (even though some of them will not be indexed). Consider creating a new index with appropriate bundle settings instead.") . '

    '; - $included_bundles = drupal_map_assoc($this->index->options['datasource']['bundles']); - foreach ($options as $bundle => $label) { - if (!isset($included_bundles[$bundle])) { - unset($options[$bundle]); - } - } - } - $form['default'] = array( - '#type' => 'radios', - '#title' => t('Which items should be indexed?'), - '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, - '#options' => array( - 1 => t('All but those from one of the selected bundles'), - 0 => t('Only those from the selected bundles'), + $form = array( + 'default' => array( + '#type' => 'radios', + '#title' => t('Which items should be indexed?'), + '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, + '#options' => array( + 1 => t('All but those from one of the selected bundles'), + 0 => t('Only those from the selected bundles'), + ), + ), + 'bundles' => array( + '#type' => 'select', + '#title' => t('Bundles'), + '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), + '#options' => $options, + '#size' => min(4, count($options)), + '#multiple' => TRUE, ), - ); - $form['bundles'] = array( - '#type' => 'select', - '#title' => t('Bundles'), - '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), - '#options' => $options, - '#size' => min(4, count($options)), - '#multiple' => TRUE, ); } else { diff --git a/includes/datasource.inc b/includes/datasource.inc index bdab0293..cf0507fd 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -281,56 +281,6 @@ interface SearchApiDataSourceControllerInterface { */ public function getEntityType(); - /** - * Form constructor for configuring the datasource for a given index. - * - * @param array $form - * The form returned by configurationForm(). - * @param array $form_state - * The form state. $form_state['index'] will contain the edited index. If - * this key is empty, then a new index is being created. In case of an edit, - * $form_state['index']->options['datasource'] contains the previous - * settings for the datasource. - * - * @return array|false - * A form array for configuring this callback, or FALSE if no configuration - * is possible. - */ - public function configurationForm(array $form, array &$form_state); - - /** - * Validation callback for the form returned by configurationForm(). - * - * This method will only be called if that form was non-empty. - * - * @param array $form - * The form returned by configurationForm(). - * @param array $values - * The part of the $form_state['values'] array corresponding to this form. - * @param array $form_state - * The complete form state. - */ - public function configurationFormValidate(array $form, array &$values, array &$form_state); - - /** - * Submit callback for the form returned by configurationForm(). - * - * This method will only be called if that form was non-empty. - * - * Any necessary changes to the submitted values should be made, afterwards - * they will automatically be stored as the index's "datasource" options. The - * method can also be used by the datasource controller to react to the - * possible change in its settings. - * - * @param array $form - * The form returned by configurationForm(). - * @param array $values - * The part of the $form_state['values'] array corresponding to this form. - * @param array $form_state - * The complete form state. - */ - public function configurationFormSubmit(array $form, array &$values, array &$form_state); - } /** @@ -571,10 +521,6 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return; } - foreach ($indexes as $index) { - $this->checkIndex($index); - } - // Since large amounts of items can overstrain the database, only add items // in chunks. foreach (array_chunk($item_ids, 1000) as $chunk) { @@ -582,6 +528,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou ->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn)); foreach ($chunk as $item_id) { foreach ($indexes as $index) { + $this->checkIndex($index); $insert->values(array( $this->itemIdColumn => $item_id, $this->indexIdColumn => $index->id, @@ -712,25 +659,6 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return array('indexed' => $indexed, 'total' => $total); } - /** - * {@inheritdoc} - */ - public function configurationForm(array $form, array &$form_state) { - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function configurationFormValidate(array $form, array &$values, array &$form_state) { - } - - /** - * {@inheritdoc} - */ - public function configurationFormSubmit(array $form, array &$values, array &$form_state) { - } - /** * Checks whether the given index is valid for this datasource controller. * diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 5f3d4895..836f57e3 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -10,69 +10,28 @@ */ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController { - /** - * Entity type info for this type. - * - * @var array - */ - protected $entityInfo; - - /** - * The ID key of this entity type, if any. - * - * @var string|null - */ - protected $idKey; - - /** - * The bundle key of this entity type, if any. - * - * @var string|null - */ - protected $bundleKey; - - /** - * Cached return values for getBundles(), keyed by index machine name. - * - * @var array - */ - protected $bundles = array(); - - /** - * {@inheritdoc} - */ - public function __construct($type) { - parent::__construct($type); - - $this->entityInfo = entity_get_info($this->entityType); - if (!empty($this->entityInfo['entity keys']['id'])) { - $this->idKey = $this->entityInfo['entity keys']['id']; - } - if (!empty($this->entityInfo['entity keys']['bundle'])) { - $this->bundleKey = $this->entityInfo['entity keys']['bundle']; - } - } - /** * {@inheritdoc} */ public function getIdFieldInfo() { + $info = entity_get_info($this->entityType); $properties = entity_get_property_info($this->entityType); - if (!$this->idKey) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $this->entityInfo['label']))); + if (empty($info['entity keys']['id'])) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label']))); } - if (empty($properties['properties'][$this->idKey]['type'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); + $field = $info['entity keys']['id']; + if (empty($properties['properties'][$field]['type'])) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field))); } - $type = $properties['properties'][$this->idKey]['type']; + $type = $properties['properties'][$field]['type']; if (search_api_is_list_type($type)) { - throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); + throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field))); } if ($type == 'token') { $type = 'string'; } return array( - 'key' => $this->idKey, + 'key' => $field, 'type' => $type, ); } @@ -144,24 +103,24 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon // all items again without any key conflicts. $this->stopTracking($indexes); - if (!empty($this->entityInfo['base table']) && $this->idKey) { + $entity_info = entity_get_info($this->entityType); + + if (!empty($entity_info['base table'])) { // Use a subselect, which will probably be much faster than entity_load(). // Assumes that all entities use the "base table" property and the // "entity keys[id]" in the same way as the default controller. - $table = $this->entityInfo['base table']; + $id_field = $entity_info['entity keys']['id']; + $table = $entity_info['base table']; - // We could also use a single insert (with a UNION in the nested query), + // We could also use a single insert (with a JOIN in the nested query), // but this method will be mostly called with a single index, anyways. foreach ($indexes as $index) { // Select all entity ids. $query = db_select($table, 't'); - $query->addField('t', $this->idKey, 'item_id'); + $query->addField('t', $id_field, 'item_id'); $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id)); $query->addExpression('1', 'changed'); - if ($bundles = $this->getIndexBundles($index)) { - $query->condition($this->bundleKey, $bundles); - } // INSERT ... SELECT ... db_insert($this->table) @@ -170,132 +129,16 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } } else { - // In the absence of a 'base table', use the slower way of retrieving the - // items and inserting them "manually". For each index we get the item IDs - // (since selected bundles might differ) and insert all of them as new. - foreach ($indexes as $index) { - $query = new EntityFieldQuery(); - $query->entityCondition('entity_type', $this->entityType); - if ($bundles = $this->getIndexBundles($index)) { - $query->entityCondition('bundle', $bundles); - } - $result = $query->execute(); - $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array(); - if ($ids) { - $this->trackItemInsert($ids, array($index)); - } - } - } - } - - /** - * {@inheritdoc} - */ - public function trackItemInsert(array $item_ids, array $indexes) { - foreach ($indexes as $index) { - $ids = $item_ids; - if ($bundles = $this->getIndexBundles($index)) { - $ids = drupal_map_assoc($ids); - foreach (entity_load($this->entityType, $ids) as $id => $entity) { - if (empty($bundles[$entity->{$this->bundleKey}])) { - unset($ids[$id]); - } - } - } - if ($ids) { - parent::trackItemInsert($ids, array($index)); - } - } - } - - /** - * {@inheritdoc} - */ - public function configurationForm(array $form, array &$form_state) { - // We don't allow later editing of the included bundles. - if (!empty($form_state['index'])) { - return FALSE; + // In the absence of a 'base table', use the slow entity_load(). + parent::startTracking($indexes); } - - $options = $this->getAvailableBundles(); - if (!$options) { - return; - } - $form['bundles'] = array( - '#type' => 'checkboxes', - '#title' => t('Bundles'), - '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed afterwards.'), - '#options' => $options, - '#attributes' => array('class' => array('search-api-checkboxes-list')), - ); - return $form; } /** * {@inheritdoc} */ - public function configurationFormSubmit(array $form, array &$values, array &$form_state) { - if (!empty($values['bundles'])) { - $values['bundles'] = array_keys(array_filter($values['bundles'])); - } - } - - /** - * Retrieves the available bundles for this entity type. - * - * @return array - * An array (which might be empty) mapping this entity type's bundle keys to - * their labels. - */ - protected function getAvailableBundles() { - if (!$this->bundleKey || empty($this->entityInfo['bundles'])) { - return array(); - } - $bundles = array(); - foreach ($this->entityInfo['bundles'] as $bundle => $bundle_info) { - $bundles[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; - } - return $bundles; - } - - /** - * Computes the bundles that should be indexed for an index. - * - * @param SearchApiIndex $index - * The index for which to check. - * - * @return array - * An array containing all bundles that should be included in this index, as - * both the keys and values. An empty array means all current bundles should - * be included. - * - * @throws SearchApiException - * If the index doesn't belong to this datasource controller. - */ - protected function getIndexBundles(SearchApiIndex $index) { - $this->checkIndex($index); - - if (!isset($this->bundles[$index->machine_name])) { - $this->bundles[$index->machine_name] = array(); - if (!empty($index->options['datasource']['bundles'])) { - // We retrieve the available bundles here to check whether all of them - // are included by the index's setting. In this case, we return an empty - // array, too, to save on complexity. - // On the other hand, we still want to return deleted bundles since we - // do not want to suddenly include all bundles when all selected bundles - // were deleted. - $available = $this->getAvailableBundles(); - foreach ($index->options['datasource']['bundles'] as $bundle) { - $this->bundles[$index->machine_name][$bundle] = $bundle; - unset($available[$bundle]); - } - if (!$available) { - $this->bundles[$index->machine_name] = array(); - } - } - } - - return $this->bundles[$index->machine_name]; + protected function getAllItemIds() { + return array_keys(entity_load($this->entityType)); } } diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 9fc1f68b..e252e1f1 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -115,8 +115,7 @@ class SearchApiIndex extends Entity { public $item_type; /** - * An array of options for configuring this index. The layout is as follows - * (with all keys being optional): + * An array of options for configuring this index. The layout is as follows: * - cron_limit: The maximum number of items to be indexed per cron batch. * - index_directly: Boolean setting whether entities are indexed immediately * after they are created or updated. @@ -151,8 +150,6 @@ class SearchApiIndex extends Entity { * - weight: Used for sorting the processors. * - settings: Processor-specific settings, configured via the processor's * configuration form. - * - datasource: Datasource-specific settings, configured via the datasource's - * configuration form. * * @var array */ diff --git a/search_api.admin.inc b/search_api.admin.inc index 71cfafc1..0654f611 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -682,126 +682,83 @@ function search_api_admin_form_delete_submit($form, &$form_state) { * * @ingroup forms * - * @see search_api_admin_add_index_ajax_callback() * @see search_api_admin_add_index_validate() * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index(array $form, array &$form_state) { drupal_set_title(t('Add index')); - $old_type = empty($form_state['values']['item_type']) ? '' : $form_state['values']['item_type']; - $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; $form['#tree'] = TRUE; + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Index name'), + '#maxlength' => 50, + '#required' => TRUE, + ); - if (empty($form_state['step_one'])) { - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Index name'), - '#maxlength' => 50, - '#required' => TRUE, - ); - - $form['machine_name'] = array( - '#type' => 'machine_name', - '#maxlength' => 50, - '#machine_name' => array( - 'exists' => 'search_api_index_load', - ), - ); + $form['machine_name'] = array( + '#type' => 'machine_name', + '#maxlength' => 50, + '#machine_name' => array( + 'exists' => 'search_api_index_load', + ), + ); - $form['item_type'] = array( - '#type' => 'select', - '#title' => t('Item type'), - '#description' => t('Select the type of items that will be indexed in this index. ' . - 'This setting cannot be changed afterwards.'), - '#options' => array(), - '#required' => TRUE, - '#ajax' => array( - 'callback' => 'search_api_admin_add_index_ajax_callback', - 'wrapper' => 'search-api-datasource-options', - ), - ); - $form['datasource'] = array(); - foreach (search_api_get_item_type_info() as $type => $info) { - $form['item_type']['#options'][$type] = $info['name']; - } - $form['enabled'] = array( - '#type' => 'checkbox', - '#title' => t('Enabled'), - '#description' => t('This will only take effect if you also select a server for the index.'), - '#default_value' => TRUE, - ); - $form['description'] = array( - '#type' => 'textarea', - '#title' => t('Index description'), - ); - $form['server'] = array( - '#type' => 'select', - '#title' => t('Server'), - '#description' => t('Select the server this index should reside on.'), - '#default_value' => '', - '#options' => array('' => t('< No server >')) - ); - $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); - // List enabled servers first. - foreach ($servers as $server) { - if ($server->enabled) { - $form['server']['#options'][$server->machine_name] = $server->name; - } - } - foreach ($servers as $server) { - if (!$server->enabled) { - $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); - } - } - $form['read_only'] = array( - '#type' => 'checkbox', - '#title' => t('Read only'), - '#description' => t('Do not write to this index or track the status of items in this index.'), - '#default_value' => FALSE, - ); - $form['options']['index_directly'] = array( - '#type' => 'checkbox', - '#title' => t('Index items immediately'), - '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . - 'This might have serious performance drawbacks and is generally not advised for larger sites.'), - '#default_value' => FALSE, - ); - $form['options']['cron_limit'] = array( - '#type' => 'textfield', - '#title' => t('Cron batch size'), - '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . - '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), - '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, - '#size' => 4, - '#attributes' => array('class' => array('search-api-cron-limit')), - '#element_validate' => array('element_validate_integer'), - ); - } - elseif (!$old_type) { - $old_type = $form_state['step_one']['item_type']; + $form['item_type'] = array( + '#type' => 'select', + '#title' => t('Item type'), + '#description' => t('Select the type of items that will be indexed in this index. ' . + 'This setting cannot be changed afterwards.'), + '#options' => array(), + '#required' => TRUE, + ); + foreach (search_api_get_item_type_info() as $type => $info) { + $form['item_type']['#options'][$type] = $info['name']; } - - if ($old_type) { - $datasource = search_api_get_datasource_controller($old_type); - $datasource_form = array(); - $datasource_form = $datasource->configurationForm($datasource_form, $form_state); - if ($datasource_form) { - $form['datasource'] = $datasource_form; - $form['datasource']['#parents'] = array('options', 'datasource'); - } + $form['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#description' => t('This will only take effect if the selected server is also enabled.'), + '#default_value' => TRUE, + ); + $form['description'] = array( + '#type' => 'textarea', + '#title' => t('Index description'), + ); + $form['server'] = array( + '#type' => 'select', + '#title' => t('Server'), + '#description' => t('Select the server this index should reside on.'), + '#default_value' => '', + '#options' => array('' => t('< No server >')) + ); + $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); + // List enabled servers first. + foreach ($servers as $server) { + $form['server']['#options'][$server->machine_name] = $server->name; } - $form['datasource']['#prefix'] = '
    '; - $form['datasource']['#suffix'] = '
    '; - - $form['old_type'] = array( - '#type' => 'value', - '#value' => $old_type, + $form['read_only'] = array( + '#type' => 'checkbox', + '#title' => t('Read only'), + '#description' => t('Do not write to this index or track the status of items in this index.'), + '#default_value' => FALSE, ); - $form['datasource_config'] = array( - '#type' => 'value', - '#value' => !empty($datasource_form), + $form['options']['index_directly'] = array( + '#type' => 'checkbox', + '#title' => t('Index items immediately'), + '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . + 'This might have serious performance drawbacks and is generally not advised for larger sites.'), + '#default_value' => FALSE, + ); + $form['options']['cron_limit'] = array( + '#type' => 'textfield', + '#title' => t('Cron batch size'), + '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . + '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), + '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, + '#size' => 4, + '#attributes' => array('class' => array('search-api-cron-limit')), ); $form['submit'] = array( @@ -812,33 +769,22 @@ function search_api_admin_add_index(array $form, array &$form_state) { return $form; } -/** - * AJAX submit callback for search_api_admin_add_index(). - * - * Used for displaying the matching datasource configuration form for the - * selected item type. - */ -function search_api_admin_add_index_ajax_callback(array $form, array &$form_state) { - return $form['datasource']; -} - /** * Form validation handler for search_api_admin_add_index(). * * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index_validate(array $form, array &$form_state) { - $values = $form_state['values']; - $name = $values['machine_name']; + $name = $form_state['values']['machine_name']; if (is_numeric($name)) { form_set_error('machine_name', t('The machine name must not be a pure number.')); } - if (!$values['datasource_config'] || empty($values['item_type']) || $values['item_type'] != $values['old_type']) { - return; + $cron_limit = $form_state['values']['options']['cron_limit']; + if ($cron_limit != '' . ((int) $cron_limit)) { + // We don't enforce stricter rules and treat all negative values as -1. + form_set_error('options[cron_limit]', t('The cron batch size must be an integer.')); } - $datasource = search_api_get_datasource_controller($values['item_type']); - $datasource->configurationFormValidate($form['datasource'], $form_state['values']['options']['datasource'], $form_state); } /** @@ -848,34 +794,10 @@ function search_api_admin_add_index_validate(array $form, array &$form_state) { */ function search_api_admin_add_index_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - $values = $form_state['values']; - if (!empty($form_state['step_one'])) { - $values += $form_state['step_one']; - unset($form_state['step_one']); - } - - // The type was changed (or the form submitted without JS for the first time). - // If the new type has a configuration form, we have to display it now. - $datasource = search_api_get_datasource_controller($values['item_type']); - if ($values['item_type'] != $values['old_type']) { - $datasource_form = array(); - if ($datasource->configurationForm($datasource_form, $form_state)) { - unset($values['options']['datasource']); - $form_state['step_one'] = $values; - $form_state['rebuild'] = TRUE; - drupal_set_message(t('Please specify further configuration options.')); - return; - } - } - - // If the current type has a configuration form, call the datasource - // controller's config submit callback. - if ($values['datasource_config']) { - $datasource->configurationFormSubmit($form['datasource'], $values['options']['datasource'], $form_state); - } + $values = $form_state['values']; - // Validation of whether a server is set for the index is done in the + // Validation of whether the server of an index is enabled is done in the // SearchApiIndex::save() method. search_api_index_insert($values); @@ -1271,7 +1193,6 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta * * @ingroup forms * - * @see search_api_admin_index_edit_validate() * @see search_api_admin_index_edit_submit() */ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) { @@ -1287,7 +1208,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#required' => TRUE, ); try { - $enabled_fixed = !$index->server(); + $enabled_fixed = !$index->enabled && !$index->server(); } catch (Exception $e) { watchdog_exception('search_api', $e); @@ -1299,7 +1220,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#type' => 'checkbox', '#title' => t('Enabled'), '#default_value' => $index->enabled, - // Can't enable an index that's not lying on any server. + // Can't enable an index lying on a disabled server, or no server at all. '#disabled' => $enabled_fixed, ); $form['description'] = array( @@ -1325,13 +1246,6 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#description' => t('Do not write to this index or track the status of items in this index.'), '#default_value' => $index->read_only, ); - - $datasource_form = !empty($form['options']['datasource']) ? $form['options']['datasource'] : array(); - $datasource_form = $index->datasource()->configurationForm($datasource_form, $form_state); - if ($datasource_form) { - $form['options']['datasource'] = $datasource_form; - } - $form['options']['index_directly'] = array( '#type' => 'checkbox', '#title' => t('Index items immediately'), @@ -1371,31 +1285,14 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI return $form; } -/** - * Form validation handler for search_api_admin_index_edit(). - * - * @see search_api_admin_index_edit_submit() - */ -function search_api_admin_index_edit_validate(array $form, array &$form_state) { - if (!empty($form['options']['datasource'])) { - $form_state['index']->datasource()->configurationFormValidate($form['options']['datasource'], $form_state['values']['options']['datasource'], $form_state); - } -} - /** * Form submission handler for search_api_admin_index_edit(). - * - * @see search_api_admin_index_edit_validate() */ function search_api_admin_index_edit_submit(array $form, array &$form_state) { form_state_values_clean($form_state); + $values = $form_state['values']; $index = $form_state['index']; - - if (!empty($form['options']['datasource'])) { - $index->datasource()->configurationFormSubmit($form['datasource'], $values['options']['datasource'], $form_state); - } - $values['options'] += $index->options; $ret = $index->update($values); diff --git a/search_api.module b/search_api.module index 72047648..2ddfa6f1 100644 --- a/search_api.module +++ b/search_api.module @@ -866,8 +866,6 @@ function search_api_entity_insert($entity, $type) { * datasource controller and calls search_api_track_item_change() for the * updated items. * - * It also checks whether the entity's bundle changed and acts accordingly. - * * @see search_api_search_api_item_type_info() */ function search_api_entity_update($entity, $type) { @@ -876,16 +874,7 @@ function search_api_entity_update($entity, $type) { if (!entity_get_property_info($type)) { return; } - list($id, , $new_bundle) = entity_extract_ids($type, $entity); - - // Check if the entity's bundle changed. - if (isset($entity->original)) { - list(, , $old_bundle) = entity_extract_ids($type, $entity->original); - if ($new_bundle != $old_bundle) { - _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle); - } - } - + list($id) = entity_extract_ids($type, $entity); if (isset($id)) { search_api_track_item_change($type, array($id)); } @@ -912,26 +901,6 @@ function search_api_entity_delete($entity, $type) { } } -/** - * Implements hook_field_attach_rename_bundle(). - * - * This is implemented on behalf of the SearchApiEntityDataSourceController - * datasource controller, to update any bundle settings that contain the changed - * bundle. - */ -function search_api_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { - foreach (search_api_index_load_multiple(FALSE, array('item_type' => $entity_type)) as $index) { - $bundles = &$index->options['datasource']['bundles']; - if (isset($bundles) && ($pos = array_search($bundle_old, $bundles)) !== FALSE) { - $bundles[$pos] = $bundle_new; - $index->save(); - // Clear all caches that could contain the bundle information. - $index->resetCaches(); - drupal_static_reset('search_api_get_datasource_controller'); - } - } -} - /** * Implements hook_field_update_field(). * @@ -2921,42 +2890,6 @@ function _search_api_deep_copy(array $array) { return $copy; } -/** - * Reacts to a change in the bundle of an entity. - * - * Used as a helper function in search_api_entity_update(). - * - * @param $type - * The entity's type. - * @param $id - * The entity's ID. - * @param $old_bundle - * The entity's previous bundle. - * @param $new_bundle - * The entity's new bundle. - */ -function _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle) { - $controller = search_api_get_datasource_controller($type); - $conditions = array( - 'enabled' => 1, - 'item_type' => $type, - 'read_only' => 0, - ); - foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { - if (!empty($index->options['datasource']['bundles'])) { - $bundles = drupal_map_assoc($index->options['datasource']['bundles']); - if (empty($bundles[$new_bundle]) != empty($bundles[$old_bundle])) { - if (empty($bundles[$new_bundle])) { - $controller->trackItemDelete(array($id), array($index)); - } - else { - $controller->trackItemInsert(array($id), array($index)); - } - } - } - } -} - /** * Creates and sets a batch for indexing items. * diff --git a/search_api.test b/search_api.test index 89883028..47479812 100644 --- a/search_api.test +++ b/search_api.test @@ -830,7 +830,6 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->index = entity_create('search_api_index', array( 'id' => 1, 'name' => 'test', - 'machine_name' => 'test', 'enabled' => 1, 'item_type' => 'user', 'options' => array( @@ -860,7 +859,6 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->checkIgnoreCaseProcessor(); $this->checkTokenizer(); $this->checkHtmlFilter(); - $this->checkEntityDatasource(); } /** @@ -1124,75 +1122,4 @@ END; $this->assertEqual($tmp[1]['name']['value'], $processed1, 'Text was correctly processed.'); } - /** - * Tests the entity datasource controller and its bundle setting. - */ - protected function checkEntityDatasource() { - // First, create the necessary content types. - $type = (object) array( - 'type' => 'article', - 'base' => 'article', - ); - node_type_save($type); - $type->type = $type->base = 'page'; - node_type_save($type); - - // Now, create some nodes. - $node = (object) array( - 'title' => 'Foo', - 'type' => 'article', - ); - node_save($node); - $nid1 = $node->nid; - $node = (object) array( - 'title' => 'Bar', - 'type' => 'article', - ); - node_save($node); - $node = (object) array( - 'title' => 'Baz', - 'type' => 'page', - ); - node_save($node); - - // We can't use $this->index here, since users don't have bundles. - $index = entity_create('search_api_index', array( - 'id' => 2, - 'name' => 'test2', - 'machine_name' => 'test2', - 'enabled' => 1, - 'item_type' => 'node', - 'options' => array( - 'fields' => array( - 'nid' => array( - 'type' => 'integer', - ), - ), - ), - )); - - // Now start tracking and check whether the index status is correct. - $datasource = search_api_get_datasource_controller('node'); - $datasource->startTracking(array($index)); - $status = $datasource->getIndexStatus($index); - $this->assertEqual($status['total'], 3, 'Correct number of items marked for indexing on not bundle-specific index.'); - $datasource->stopTracking(array($index)); - - // Once again, but with only indexing articles. - $index->options['datasource']['bundles'] = array('article'); - drupal_static_reset('search_api_get_datasource_controller'); - $datasource = search_api_get_datasource_controller('node'); - $datasource->startTracking(array($index)); - $status = $datasource->getIndexStatus($index); - $this->assertEqual($status['total'], 2, 'Correct number of items marked for indexing on bundle-specific index.'); - $datasource->stopTracking(array($index)); - - // Now test that bundle renaming works. - $index->save(); - field_attach_rename_bundle('node', 'article', 'foo'); - $index = search_api_index_load('test2', TRUE); - $this->assertEqual($index->options['datasource']['bundles'], array('foo'), 'Bundle was correctly renamed in index settings.'); - $index->delete(); - } - } From 968ec5a2027e92e23321981799a1c799b2bdede9 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 26 Dec 2014 08:37:34 +0100 Subject: [PATCH 137/278] Adapted CHANGELOG.txt to 1.14 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index aa29fd42..d1980c91 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.14 (12/26/2014): +----------------------------- - #2382385 by illusionuk, drunken monkey: Fixed error handling when using invalid fulltext or sort field in Views. - #2371099 by drunken monkey: Fixed display of active "Exclude" facets. From c5f713533c6a163e9f1ba9e7530bda80b406e3a6 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 26 Dec 2014 08:38:11 +0100 Subject: [PATCH 138/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d1980c91..f64b6e8c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.14 (12/26/2014): ----------------------------- - #2382385 by illusionuk, drunken monkey: Fixed error handling when using From 8e468fda8f1a6c983e252429678b6f1af791fe0f Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 27 Jan 2015 14:04:03 +0100 Subject: [PATCH 139/278] Issue #2387161 by drunken monkey: Added a hook for altering search results. --- CHANGELOG.txt | 1 + includes/query.inc | 3 +++ search_api.api.php | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f64b6e8c..4bdb6cf0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2387161 by drunken monkey: Added a hook for altering search results. Search API 1.14 (12/26/2014): ----------------------------- diff --git a/includes/query.inc b/includes/query.inc index b887c795..b005cf73 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -720,6 +720,9 @@ class SearchApiQuery implements SearchApiQueryInterface { public function postExecute(array &$results) { // Postprocess results. $this->index->postprocessSearchResults($results, $this); + + // Let modules alter the results. + drupal_alter('search_api_results', $results, $this); } /** diff --git a/search_api.api.php b/search_api.api.php index 550ecbc2..f9821081 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -345,6 +345,26 @@ function hook_search_api_query_alter(SearchApiQueryInterface $query) { } } +/** + * Alter the search results before they are returned. + * + * @param array $results + * The results returned by the server, which may be altered. The data + * structure is the same as returned by SearchApiQueryInterface::execute(). + * @param SearchApiQueryInterface $query + * The search query that was executed. + */ +function hook_search_api_results_alter(array &$results, SearchApiQueryInterface $query) { + if ($query->getOption('search id') == 'search_api_views:my_search_view:page') { + // Log the number of results. + $vars = array( + '@keys' => $query->getOriginalKeys(), + '@num' => $results['result count'], + ); + watchdog('my_module', 'Search view with query "@keys" had @num results.', $vars, WATCHDOG_DEBUG); + } +} + /** * Act on search servers when they are loaded. * From ad51ddaf060f5830f8e9f0274cfa84878ebce6bd Mon Sep 17 00:00:00 2001 From: darrenoh Date: Tue, 27 Jan 2015 18:48:11 +0100 Subject: [PATCH 140/278] Issue #2414367 by Darren Oh, drunken monkey: Fixed detection of missing fields in Views. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4bdb6cf0..23f85897 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2414367 by Darren Oh, drunken monkey: Fixed detection of missing fields in + Views. - #2387161 by drunken monkey: Added a hook for altering search results. Search API 1.14 (12/26/2014): diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 9356bbab..44716adc 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -400,7 +400,7 @@ class SearchApiViewsQuery extends views_plugin_query { } // Check whether we need to extract any properties from the result item. - $missing_fields = array_diff_key($this->fields, $row); + $missing_fields = array_diff_key($this->fields, $row['_entity_properties']); if ($missing_fields) { $missing[$id] = $missing_fields; if (is_object($row['entity'])) { @@ -418,14 +418,14 @@ class SearchApiViewsQuery extends views_plugin_query { // Load items of those rows which haven't got all field values, yet. if (!empty($ids)) { $items += $this->index->loadItems($ids); - // $items now includes loaded items, and those already passed in the - // search results. - foreach ($items as $id => $item) { - // Extract item properties. - $wrapper = $this->index->entityWrapper($item, FALSE); - $rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]); - $rows[$id]->entity = $item; - } + } + // $items now includes all loaded items from which fields still need to be + // extracted. + foreach ($items as $id => $item) { + // Extract item properties. + $wrapper = $this->index->entityWrapper($item, FALSE); + $rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]); + $rows[$id]->entity = $item; } // Finally, add all rows to the Views result set. From fe7232b443be744b7b010be702a9362c218b551c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 3 Mar 2015 18:26:57 +0100 Subject: [PATCH 141/278] Issue #2412895 by drunken monkey: Fixed entity load for Views entity access check. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 23f85897..e8373cdd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2412895 by drunken monkey: Fixed entity load for Views entity access check. - #2414367 by Darren Oh, drunken monkey: Fixed detection of missing fields in Views. - #2387161 by drunken monkey: Added a hook for altering search results. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 44716adc..1af1f089 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -375,8 +375,8 @@ class SearchApiViewsQuery extends views_plugin_query { // loading any items. foreach ($results as $id => $result) { if (!empty($this->options['entity_access']) && ($entity_type = $this->index->getEntityType())) { - $entity = entity_load($entity_type, array($id)); - if (!entity_access('view', $entity_type, $entity[$id])) { + $entity = $this->index->loadItems(array($id)); + if (!$entity || !entity_access('view', $entity_type, reset($entity))) { continue; } } From fee782cdada74cc82cfef9a4381d1920f4fdec83 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 5 Mar 2015 14:32:21 +0100 Subject: [PATCH 142/278] Issue #1396222 by drunken monkey: Added a "First letter" aggregation type to the "Aggregated fields" data alteration. --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e8373cdd..24f1647d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1396222 by drunken monkey: Added a "First letter" aggregation type to the + "Aggregated fields" data alteration. - #2412895 by drunken monkey: Fixed entity load for Views entity access check. - #2414367 by Darren Oh, drunken monkey: Fixed detection of missing fields in Views. diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index d1ed9bce..8ae612cd 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -10,6 +10,16 @@ */ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { + /** + * The type of aggregation currently performed. + * + * Used to temporarily store the current aggregation type for use of + * SearchApiAlterAddAggregation::reduce() with array_reduce(). + * + * @var string + */ + protected $reductionType; + public function configurationForm() { $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; @@ -193,6 +203,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { return isset($a) ? min($a, $b) : $b; case 'first': return isset($a) ? $a : $b; + case 'first_char': + $b = "$b"; + if (isset($a) || $b === '') { + return $a; + } + return drupal_substr($b, 0, 1); case 'list': if (!isset($a)) { $a = array(); @@ -200,6 +216,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { $a[] = $b; return $a; } + return NULL; } /** @@ -267,6 +284,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => t('Maximum'), 'min' => t('Minimum'), 'first' => t('First'), + 'first_char' => t('First letter'), 'list' => t('List'), ); case 'type': @@ -277,6 +295,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => 'integer', 'min' => 'integer', 'first' => 'string', + 'first_char' => 'string', 'list' => 'list', ); case 'description': @@ -287,6 +306,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => t('The Maximum aggregation computes the numerically largest contained field value.'), 'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'), 'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'), + 'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'), 'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'), ); } From b09c1b5b6d9f29ab15ced1aee237f8e5c5fa0f92 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 3 Apr 2015 14:10:45 +0200 Subject: [PATCH 143/278] Issue #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. --- CHANGELOG.txt | 2 + includes/callback_bundle_filter.inc | 45 +++-- includes/datasource.inc | 98 ++++++++++- includes/datasource_entity.inc | 212 +++++++++++++++++++--- includes/index_entity.inc | 5 +- search_api.admin.inc | 264 ++++++++++++++++++++-------- search_api.module | 75 +++++++- search_api.test | 73 ++++++++ 8 files changed, 658 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 24f1647d..874aa279 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1184610 by drunken monkey: Added option to limit indexes to specific entity + bundles. - #1396222 by drunken monkey: Added a "First letter" aggregation type to the "Aggregated fields" data alteration. - #2412895 by drunken monkey: Fixed entity load for Views entity access check. diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index 576fa608..08fcba45 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -14,7 +14,9 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * {@inheritdoc} */ public function supportsIndex(SearchApiIndex $index) { - return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); + $support = $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); + $support &= empty($index->options['datasource']['bundles']) || count($index->options['datasource']['bundles']) > 1; + return $support; } /** @@ -44,25 +46,32 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { foreach ($info['bundles'] as $bundle => $bundle_info) { $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; } - $form = array( - 'default' => array( - '#type' => 'radios', - '#title' => t('Which items should be indexed?'), - '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, - '#options' => array( - 1 => t('All but those from one of the selected bundles'), - 0 => t('Only those from the selected bundles'), - ), - ), - 'bundles' => array( - '#type' => 'select', - '#title' => t('Bundles'), - '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), - '#options' => $options, - '#size' => min(4, count($options)), - '#multiple' => TRUE, + if (!empty($this->index->options['datasource']['bundles'])) { + $form['message']['#markup'] = '

    ' . t("Note: This index is already restricted to certain bundles. If you use this data alteration, those will be reduced further. However, the index setting is better supported in the user interface and should therefore be prefered. For example, using this data alteration will not reduce the displayed total number of items to index (even though some of them will not be indexed). Consider creating a new index with appropriate bundle settings instead.") . '

    '; + $included_bundles = drupal_map_assoc($this->index->options['datasource']['bundles']); + foreach ($options as $bundle => $label) { + if (!isset($included_bundles[$bundle])) { + unset($options[$bundle]); + } + } + } + $form['default'] = array( + '#type' => 'radios', + '#title' => t('Which items should be indexed?'), + '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1, + '#options' => array( + 1 => t('All but those from one of the selected bundles'), + 0 => t('Only those from the selected bundles'), ), ); + $form['bundles'] = array( + '#type' => 'select', + '#title' => t('Bundles'), + '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(), + '#options' => $options, + '#size' => min(4, count($options)), + '#multiple' => TRUE, + ); } else { $form = array( diff --git a/includes/datasource.inc b/includes/datasource.inc index cf0507fd..bac2a658 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -158,6 +158,10 @@ interface SearchApiDataSourceControllerInterface { * @param SearchApiIndex[] $indexes * The indexes for which items should be tracked. * + * @return SearchApiIndex[]|null + * All indexes for which any items were added; or NULL if items were added + * for all of them. + * * @throws SearchApiDataSourceException * If any error state was encountered. */ @@ -281,6 +285,69 @@ interface SearchApiDataSourceControllerInterface { */ public function getEntityType(); + /** + * Form constructor for configuring the datasource for a given index. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $form_state + * The form state. $form_state['index'] will contain the edited index. If + * this key is empty, then a new index is being created. In case of an edit, + * $form_state['index']->options['datasource'] contains the previous + * settings for the datasource. + * + * @return array|false + * A form array for configuring this callback, or FALSE if no configuration + * is possible. + */ + public function configurationForm(array $form, array &$form_state); + + /** + * Validation callback for the form returned by configurationForm(). + * + * This method will only be called if that form was non-empty. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $values + * The part of the $form_state['values'] array corresponding to this form. + * @param array $form_state + * The complete form state. + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state); + + /** + * Submit callback for the form returned by configurationForm(). + * + * This method will only be called if that form was non-empty. + * + * Any necessary changes to the submitted values should be made, afterwards + * they will automatically be stored as the index's "datasource" options. The + * method can also be used by the datasource controller to react to the + * possible change in its settings. + * + * @param array $form + * The form returned by configurationForm(). + * @param array $values + * The part of the $form_state['values'] array corresponding to this form. + * @param array $form_state + * The complete form state. + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state); + + /** + * Returns a summary of an index's current datasource configuration. + * + * @param SearchApiIndex $index + * The index whose datasource configuration should be summarized. + * + * @return string|null + * A translated string describing the index's current datasource + * configuration. Or NULL, if there is no configuration (or no description + * is available). + */ + public function getConfigurationSummary(SearchApiIndex $index); + } /** @@ -521,6 +588,10 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return; } + foreach ($indexes as $index) { + $this->checkIndex($index); + } + // Since large amounts of items can overstrain the database, only add items // in chunks. foreach (array_chunk($item_ids, 1000) as $chunk) { @@ -528,7 +599,6 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou ->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn)); foreach ($chunk as $item_id) { foreach ($indexes as $index) { - $this->checkIndex($index); $insert->values(array( $this->itemIdColumn => $item_id, $this->indexIdColumn => $index->id, @@ -659,6 +729,32 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return array('indexed' => $indexed, 'total' => $total); } + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function getConfigurationSummary(SearchApiIndex $index) { + return NULL; + } + /** * Checks whether the given index is valid for this datasource controller. * diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 836f57e3..ddfd34ac 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -10,28 +10,69 @@ */ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController { + /** + * Entity type info for this type. + * + * @var array + */ + protected $entityInfo; + + /** + * The ID key of this entity type, if any. + * + * @var string|null + */ + protected $idKey; + + /** + * The bundle key of this entity type, if any. + * + * @var string|null + */ + protected $bundleKey; + + /** + * Cached return values for getBundles(), keyed by index machine name. + * + * @var array + */ + protected $bundles = array(); + + /** + * {@inheritdoc} + */ + public function __construct($type) { + parent::__construct($type); + + $this->entityInfo = entity_get_info($this->entityType); + if (!empty($this->entityInfo['entity keys']['id'])) { + $this->idKey = $this->entityInfo['entity keys']['id']; + } + if (!empty($this->entityInfo['entity keys']['bundle'])) { + $this->bundleKey = $this->entityInfo['entity keys']['bundle']; + } + } + /** * {@inheritdoc} */ public function getIdFieldInfo() { - $info = entity_get_info($this->entityType); $properties = entity_get_property_info($this->entityType); - if (empty($info['entity keys']['id'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label']))); + if (!$this->idKey) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $this->entityInfo['label']))); } - $field = $info['entity keys']['id']; - if (empty($properties['properties'][$field]['type'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field))); + if (empty($properties['properties'][$this->idKey]['type'])) { + throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); } - $type = $properties['properties'][$field]['type']; + $type = $properties['properties'][$this->idKey]['type']; if (search_api_is_list_type($type)) { - throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field))); + throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey))); } if ($type == 'token') { $type = 'string'; } return array( - 'key' => $field, + 'key' => $this->idKey, 'type' => $type, ); } @@ -103,24 +144,24 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon // all items again without any key conflicts. $this->stopTracking($indexes); - $entity_info = entity_get_info($this->entityType); - - if (!empty($entity_info['base table'])) { + if (!empty($this->entityInfo['base table']) && $this->idKey) { // Use a subselect, which will probably be much faster than entity_load(). // Assumes that all entities use the "base table" property and the // "entity keys[id]" in the same way as the default controller. - $id_field = $entity_info['entity keys']['id']; - $table = $entity_info['base table']; + $table = $this->entityInfo['base table']; - // We could also use a single insert (with a JOIN in the nested query), + // We could also use a single insert (with a UNION in the nested query), // but this method will be mostly called with a single index, anyways. foreach ($indexes as $index) { // Select all entity ids. $query = db_select($table, 't'); - $query->addField('t', $id_field, 'item_id'); + $query->addField('t', $this->idKey, 'item_id'); $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id)); $query->addExpression('1', 'changed'); + if ($bundles = $this->getIndexBundles($index)) { + $query->condition($this->bundleKey, $bundles); + } // INSERT ... SELECT ... db_insert($this->table) @@ -129,16 +170,147 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } } else { - // In the absence of a 'base table', use the slow entity_load(). - parent::startTracking($indexes); + // In the absence of a 'base table', use the slower way of retrieving the + // items and inserting them "manually". For each index we get the item IDs + // (since selected bundles might differ) and insert all of them as new. + foreach ($indexes as $index) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', $this->entityType); + if ($bundles = $this->getIndexBundles($index)) { + $query->entityCondition('bundle', $bundles); + } + $result = $query->execute(); + $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array(); + if ($ids) { + $this->trackItemInsert($ids, array($index)); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function trackItemInsert(array $item_ids, array $indexes) { + $ret = array(); + + foreach ($indexes as $index_id => $index) { + $ids = $item_ids; + if ($bundles = $this->getIndexBundles($index)) { + $ids = drupal_map_assoc($ids); + foreach (entity_load($this->entityType, $ids) as $id => $entity) { + if (empty($bundles[$entity->{$this->bundleKey}])) { + unset($ids[$id]); + } + } + } + if ($ids) { + parent::trackItemInsert($ids, array($index)); + $ret[$index_id] = $index; + } + } + + return $ret; + } + + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + $options = $this->getAvailableBundles(); + if (!$options) { + return FALSE; + } + $form['bundles'] = array( + '#type' => 'checkboxes', + '#title' => t('Bundles'), + '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'), + '#options' => $options, + '#attributes' => array('class' => array('search-api-checkboxes-list')), + '#disabled' => !empty($form_state['index']), + ); + if (!empty($form_state['index'])) { + $form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']); + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + if (!empty($values['bundles'])) { + $values['bundles'] = array_keys(array_filter($values['bundles'])); } } /** * {@inheritdoc} */ - protected function getAllItemIds() { - return array_keys(entity_load($this->entityType)); + public function getConfigurationSummary(SearchApiIndex $index) { + if ($bundles = $this->getIndexBundles($index)) { + $args['!bundles'] = implode(', ', array_intersect_key($this->getAvailableBundles(), $bundles)); + return format_plural(count($bundles), 'Indexed bundle: !bundles.', 'Indexed bundles: !bundles.', $args); + } + return NULL; + } + + /** + * Retrieves the available bundles for this entity type. + * + * @return array + * An array (which might be empty) mapping this entity type's bundle keys to + * their labels. + */ + protected function getAvailableBundles() { + if (!$this->bundleKey || empty($this->entityInfo['bundles'])) { + return array(); + } + $bundles = array(); + foreach ($this->entityInfo['bundles'] as $bundle => $bundle_info) { + $bundles[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; + } + return $bundles; + } + + /** + * Computes the bundles that should be indexed for an index. + * + * @param SearchApiIndex $index + * The index for which to check. + * + * @return array + * An array containing all bundles that should be included in this index, as + * both the keys and values. An empty array means all current bundles should + * be included. + * + * @throws SearchApiException + * If the index doesn't belong to this datasource controller. + */ + protected function getIndexBundles(SearchApiIndex $index) { + $this->checkIndex($index); + + if (!isset($this->bundles[$index->machine_name])) { + $this->bundles[$index->machine_name] = array(); + if (!empty($index->options['datasource']['bundles'])) { + // We retrieve the available bundles here to check whether all of them + // are included by the index's setting. In this case, we return an empty + // array, too, to save on complexity. + // On the other hand, we still want to return deleted bundles since we + // do not want to suddenly include all bundles when all selected bundles + // were deleted. + $available = $this->getAvailableBundles(); + foreach ($index->options['datasource']['bundles'] as $bundle) { + $this->bundles[$index->machine_name][$bundle] = $bundle; + unset($available[$bundle]); + } + if (!$available) { + $this->bundles[$index->machine_name] = array(); + } + } + } + + return $this->bundles[$index->machine_name]; } } diff --git a/includes/index_entity.inc b/includes/index_entity.inc index e252e1f1..9fc1f68b 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -115,7 +115,8 @@ class SearchApiIndex extends Entity { public $item_type; /** - * An array of options for configuring this index. The layout is as follows: + * An array of options for configuring this index. The layout is as follows + * (with all keys being optional): * - cron_limit: The maximum number of items to be indexed per cron batch. * - index_directly: Boolean setting whether entities are indexed immediately * after they are created or updated. @@ -150,6 +151,8 @@ class SearchApiIndex extends Entity { * - weight: Used for sorting the processors. * - settings: Processor-specific settings, configured via the processor's * configuration form. + * - datasource: Datasource-specific settings, configured via the datasource's + * configuration form. * * @var array */ diff --git a/search_api.admin.inc b/search_api.admin.inc index 0654f611..3cb9b4f3 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -682,83 +682,126 @@ function search_api_admin_form_delete_submit($form, &$form_state) { * * @ingroup forms * + * @see search_api_admin_add_index_ajax_callback() * @see search_api_admin_add_index_validate() * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index(array $form, array &$form_state) { drupal_set_title(t('Add index')); + $old_type = empty($form_state['values']['item_type']) ? '' : $form_state['values']['item_type']; + $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; $form['#tree'] = TRUE; - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Index name'), - '#maxlength' => 50, - '#required' => TRUE, - ); - $form['machine_name'] = array( - '#type' => 'machine_name', - '#maxlength' => 50, - '#machine_name' => array( - 'exists' => 'search_api_index_load', - ), - ); + if (empty($form_state['step_one'])) { + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Index name'), + '#maxlength' => 50, + '#required' => TRUE, + ); - $form['item_type'] = array( - '#type' => 'select', - '#title' => t('Item type'), - '#description' => t('Select the type of items that will be indexed in this index. ' . - 'This setting cannot be changed afterwards.'), - '#options' => array(), - '#required' => TRUE, - ); - foreach (search_api_get_item_type_info() as $type => $info) { - $form['item_type']['#options'][$type] = $info['name']; + $form['machine_name'] = array( + '#type' => 'machine_name', + '#maxlength' => 50, + '#machine_name' => array( + 'exists' => 'search_api_index_load', + ), + ); + + $form['item_type'] = array( + '#type' => 'select', + '#title' => t('Item type'), + '#description' => t('Select the type of items that will be indexed in this index. ' . + 'This setting cannot be changed afterwards.'), + '#options' => array(), + '#required' => TRUE, + '#ajax' => array( + 'callback' => 'search_api_admin_add_index_ajax_callback', + 'wrapper' => 'search-api-datasource-options', + ), + ); + $form['datasource'] = array(); + foreach (search_api_get_item_type_info() as $type => $info) { + $form['item_type']['#options'][$type] = $info['name']; + } + $form['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#description' => t('This will only take effect if you also select a server for the index.'), + '#default_value' => TRUE, + ); + $form['description'] = array( + '#type' => 'textarea', + '#title' => t('Index description'), + ); + $form['server'] = array( + '#type' => 'select', + '#title' => t('Server'), + '#description' => t('Select the server this index should reside on.'), + '#default_value' => '', + '#options' => array('' => t('< No server >')) + ); + $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); + // List enabled servers first. + foreach ($servers as $server) { + if ($server->enabled) { + $form['server']['#options'][$server->machine_name] = $server->name; + } + } + foreach ($servers as $server) { + if (!$server->enabled) { + $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); + } + } + $form['read_only'] = array( + '#type' => 'checkbox', + '#title' => t('Read only'), + '#description' => t('Do not write to this index or track the status of items in this index.'), + '#default_value' => FALSE, + ); + $form['options']['index_directly'] = array( + '#type' => 'checkbox', + '#title' => t('Index items immediately'), + '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . + 'This might have serious performance drawbacks and is generally not advised for larger sites.'), + '#default_value' => FALSE, + ); + $form['options']['cron_limit'] = array( + '#type' => 'textfield', + '#title' => t('Cron batch size'), + '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . + '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), + '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, + '#size' => 4, + '#attributes' => array('class' => array('search-api-cron-limit')), + '#element_validate' => array('element_validate_integer'), + ); } - $form['enabled'] = array( - '#type' => 'checkbox', - '#title' => t('Enabled'), - '#description' => t('This will only take effect if the selected server is also enabled.'), - '#default_value' => TRUE, - ); - $form['description'] = array( - '#type' => 'textarea', - '#title' => t('Index description'), - ); - $form['server'] = array( - '#type' => 'select', - '#title' => t('Server'), - '#description' => t('Select the server this index should reside on.'), - '#default_value' => '', - '#options' => array('' => t('< No server >')) - ); - $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1)); - // List enabled servers first. - foreach ($servers as $server) { - $form['server']['#options'][$server->machine_name] = $server->name; + elseif (!$old_type) { + $old_type = $form_state['step_one']['item_type']; } - $form['read_only'] = array( - '#type' => 'checkbox', - '#title' => t('Read only'), - '#description' => t('Do not write to this index or track the status of items in this index.'), - '#default_value' => FALSE, - ); - $form['options']['index_directly'] = array( - '#type' => 'checkbox', - '#title' => t('Index items immediately'), - '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . - 'This might have serious performance drawbacks and is generally not advised for larger sites.'), - '#default_value' => FALSE, + + if ($old_type) { + $datasource = search_api_get_datasource_controller($old_type); + $datasource_form = array(); + $datasource_form = $datasource->configurationForm($datasource_form, $form_state); + if ($datasource_form) { + $form['datasource'] = $datasource_form; + $form['datasource']['#parents'] = array('options', 'datasource'); + } + } + $form['datasource']['#prefix'] = '
    '; + $form['datasource']['#suffix'] = '
    '; + + $form['old_type'] = array( + '#type' => 'value', + '#value' => $old_type, ); - $form['options']['cron_limit'] = array( - '#type' => 'textfield', - '#title' => t('Cron batch size'), - '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' . - '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'), - '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT, - '#size' => 4, - '#attributes' => array('class' => array('search-api-cron-limit')), + $form['datasource_config'] = array( + '#type' => 'value', + '#value' => !empty($datasource_form), ); $form['submit'] = array( @@ -769,22 +812,33 @@ function search_api_admin_add_index(array $form, array &$form_state) { return $form; } +/** + * AJAX submit callback for search_api_admin_add_index(). + * + * Used for displaying the matching datasource configuration form for the + * selected item type. + */ +function search_api_admin_add_index_ajax_callback(array $form, array &$form_state) { + return $form['datasource']; +} + /** * Form validation handler for search_api_admin_add_index(). * * @see search_api_admin_add_index_submit() */ function search_api_admin_add_index_validate(array $form, array &$form_state) { - $name = $form_state['values']['machine_name']; + $values = $form_state['values']; + $name = $values['machine_name']; if (is_numeric($name)) { form_set_error('machine_name', t('The machine name must not be a pure number.')); } - $cron_limit = $form_state['values']['options']['cron_limit']; - if ($cron_limit != '' . ((int) $cron_limit)) { - // We don't enforce stricter rules and treat all negative values as -1. - form_set_error('options[cron_limit]', t('The cron batch size must be an integer.')); + if (!$values['datasource_config'] || empty($values['item_type']) || $values['item_type'] != $values['old_type']) { + return; } + $datasource = search_api_get_datasource_controller($values['item_type']); + $datasource->configurationFormValidate($form['datasource'], $form_state['values']['options']['datasource'], $form_state); } /** @@ -794,10 +848,34 @@ function search_api_admin_add_index_validate(array $form, array &$form_state) { */ function search_api_admin_add_index_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - $values = $form_state['values']; - // Validation of whether the server of an index is enabled is done in the + if (!empty($form_state['step_one'])) { + $values += $form_state['step_one']; + unset($form_state['step_one']); + } + + // The type was changed (or the form submitted without JS for the first time). + // If the new type has a configuration form, we have to display it now. + $datasource = search_api_get_datasource_controller($values['item_type']); + if ($values['item_type'] != $values['old_type']) { + $datasource_form = array(); + if ($datasource->configurationForm($datasource_form, $form_state)) { + unset($values['options']['datasource']); + $form_state['step_one'] = $values; + $form_state['rebuild'] = TRUE; + drupal_set_message(t('Please specify further configuration options.')); + return; + } + } + + // If the current type has a configuration form, call the datasource + // controller's config submit callback. + if ($values['datasource_config']) { + $datasource->configurationFormSubmit($form['datasource'], $values['options']['datasource'], $form_state); + } + + // Validation of whether a server is set for the index is done in the // SearchApiIndex::save() method. search_api_index_insert($values); @@ -857,6 +935,7 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { '#machine_name' => $index->machine_name, '#description' => $index->description, '#item_type' => $index->item_type, + '#datasource_config' => $index->datasource()->getConfigurationSummary($index), '#enabled' => $index->enabled, '#server' => $server, '#options' => $index->options, @@ -889,6 +968,7 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) { * - machine_name: The index' machine name. * - description: The index' description. * - item_type: The type of items stored in this index. + * - datasource_config: A summary of the datasource's configuration. * - enabled: Boolean indicating whether the index is enabled. * - server: The server this index currently rests on, if any. * - options: The index' options, like cron limit. @@ -912,6 +992,7 @@ function theme_search_api_index(array $variables) { $description = $variables['description']; $enabled = $variables['enabled']; $item_type = $variables['item_type']; + $datasource_config = $variables['datasource_config']; $server = $variables['server']; $options = $variables['options']; $status = $variables['status']; @@ -962,6 +1043,12 @@ function theme_search_api_index(array $variables) { $info = check_plain($item_type); $rows[] = _search_api_deep_copy($row); + if ($datasource_config) { + $label = t('Item type configuration'); + $info = check_plain($datasource_config); + $rows[] = _search_api_deep_copy($row); + } + if ($server) { $label = t('Server'); $info = l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); @@ -1193,6 +1280,7 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta * * @ingroup forms * + * @see search_api_admin_index_edit_validate() * @see search_api_admin_index_edit_submit() */ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) { @@ -1208,7 +1296,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#required' => TRUE, ); try { - $enabled_fixed = !$index->enabled && !$index->server(); + $enabled_fixed = !$index->server(); } catch (Exception $e) { watchdog_exception('search_api', $e); @@ -1220,7 +1308,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#type' => 'checkbox', '#title' => t('Enabled'), '#default_value' => $index->enabled, - // Can't enable an index lying on a disabled server, or no server at all. + // Can't enable an index that's not lying on any server. '#disabled' => $enabled_fixed, ); $form['description'] = array( @@ -1240,6 +1328,15 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI foreach ($servers as $server) { $form['server']['#options'][$server->machine_name] = $server->name; } + + $datasource_form = !empty($form['options']['datasource']) ? $form['options']['datasource'] : array(); + $datasource_form = $index->datasource()->configurationForm($datasource_form, $form_state); + if ($datasource_form) { + $form['options']['datasource'] = $datasource_form; + $form['options']['datasource']['#type'] = 'fieldset'; + $form['options']['datasource']['#title'] = t('Datasource options'); + } + $form['read_only'] = array( '#type' => 'checkbox', '#title' => t('Read only'), @@ -1285,14 +1382,33 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI return $form; } +/** + * Form validation handler for search_api_admin_index_edit(). + * + * @see search_api_admin_index_edit_submit() + */ +function search_api_admin_index_edit_validate(array $form, array &$form_state) { + if (!empty($form['options']['datasource'])) { + $form_state['values']['options'] += array('datasource' => array()); + $form_state['index']->datasource()->configurationFormValidate($form['options']['datasource'], $form_state['values']['options']['datasource'], $form_state); + } +} + /** * Form submission handler for search_api_admin_index_edit(). + * + * @see search_api_admin_index_edit_validate() */ function search_api_admin_index_edit_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - $values = $form_state['values']; + /** @var SearchApiIndex $index */ $index = $form_state['index']; + + if (!empty($form['options']['datasource'])) { + $index->datasource()->configurationFormSubmit($form['options']['datasource'], $values['options']['datasource'], $form_state); + } + $values['options'] += $index->options; $ret = $index->update($values); diff --git a/search_api.module b/search_api.module index 2ddfa6f1..d145844d 100644 --- a/search_api.module +++ b/search_api.module @@ -270,6 +270,7 @@ function search_api_theme() { 'machine_name' => '', 'description' => NULL, 'item_type' => NULL, + 'datasource_config' => NULL, 'enabled' => NULL, 'server' => NULL, 'options' => array(), @@ -866,6 +867,8 @@ function search_api_entity_insert($entity, $type) { * datasource controller and calls search_api_track_item_change() for the * updated items. * + * It also checks whether the entity's bundle changed and acts accordingly. + * * @see search_api_search_api_item_type_info() */ function search_api_entity_update($entity, $type) { @@ -874,7 +877,16 @@ function search_api_entity_update($entity, $type) { if (!entity_get_property_info($type)) { return; } - list($id) = entity_extract_ids($type, $entity); + list($id, , $new_bundle) = entity_extract_ids($type, $entity); + + // Check if the entity's bundle changed. + if (isset($entity->original)) { + list(, , $old_bundle) = entity_extract_ids($type, $entity->original); + if ($new_bundle != $old_bundle) { + _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle); + } + } + if (isset($id)) { search_api_track_item_change($type, array($id)); } @@ -901,6 +913,26 @@ function search_api_entity_delete($entity, $type) { } } +/** + * Implements hook_field_attach_rename_bundle(). + * + * This is implemented on behalf of the SearchApiEntityDataSourceController + * datasource controller, to update any bundle settings that contain the changed + * bundle. + */ +function search_api_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { + foreach (search_api_index_load_multiple(FALSE, array('item_type' => $entity_type)) as $index) { + $bundles = &$index->options['datasource']['bundles']; + if (isset($bundles) && ($pos = array_search($bundle_old, $bundles)) !== FALSE) { + $bundles[$pos] = $bundle_new; + $index->save(); + // Clear all caches that could contain the bundle information. + $index->resetCaches(); + drupal_static_reset('search_api_get_datasource_controller'); + } + } +} + /** * Implements hook_field_update_field(). * @@ -1100,7 +1132,10 @@ function search_api_track_item_insert($type, array $item_ids) { } try { - search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes); + $returned_indexes = search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes); + if (isset($returned_indexes)) { + $indexes = $returned_indexes; + } } catch (SearchApiException $e) { $vars['%item_type'] = $type; @@ -2890,6 +2925,42 @@ function _search_api_deep_copy(array $array) { return $copy; } +/** + * Reacts to a change in the bundle of an entity. + * + * Used as a helper function in search_api_entity_update(). + * + * @param $type + * The entity's type. + * @param $id + * The entity's ID. + * @param $old_bundle + * The entity's previous bundle. + * @param $new_bundle + * The entity's new bundle. + */ +function _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle) { + $controller = search_api_get_datasource_controller($type); + $conditions = array( + 'enabled' => 1, + 'item_type' => $type, + 'read_only' => 0, + ); + foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { + if (!empty($index->options['datasource']['bundles'])) { + $bundles = drupal_map_assoc($index->options['datasource']['bundles']); + if (empty($bundles[$new_bundle]) != empty($bundles[$old_bundle])) { + if (empty($bundles[$new_bundle])) { + $controller->trackItemDelete(array($id), array($index)); + } + else { + $controller->trackItemInsert(array($id), array($index)); + } + } + } + } +} + /** * Creates and sets a batch for indexing items. * diff --git a/search_api.test b/search_api.test index 47479812..89883028 100644 --- a/search_api.test +++ b/search_api.test @@ -830,6 +830,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->index = entity_create('search_api_index', array( 'id' => 1, 'name' => 'test', + 'machine_name' => 'test', 'enabled' => 1, 'item_type' => 'user', 'options' => array( @@ -859,6 +860,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { $this->checkIgnoreCaseProcessor(); $this->checkTokenizer(); $this->checkHtmlFilter(); + $this->checkEntityDatasource(); } /** @@ -1122,4 +1124,75 @@ END; $this->assertEqual($tmp[1]['name']['value'], $processed1, 'Text was correctly processed.'); } + /** + * Tests the entity datasource controller and its bundle setting. + */ + protected function checkEntityDatasource() { + // First, create the necessary content types. + $type = (object) array( + 'type' => 'article', + 'base' => 'article', + ); + node_type_save($type); + $type->type = $type->base = 'page'; + node_type_save($type); + + // Now, create some nodes. + $node = (object) array( + 'title' => 'Foo', + 'type' => 'article', + ); + node_save($node); + $nid1 = $node->nid; + $node = (object) array( + 'title' => 'Bar', + 'type' => 'article', + ); + node_save($node); + $node = (object) array( + 'title' => 'Baz', + 'type' => 'page', + ); + node_save($node); + + // We can't use $this->index here, since users don't have bundles. + $index = entity_create('search_api_index', array( + 'id' => 2, + 'name' => 'test2', + 'machine_name' => 'test2', + 'enabled' => 1, + 'item_type' => 'node', + 'options' => array( + 'fields' => array( + 'nid' => array( + 'type' => 'integer', + ), + ), + ), + )); + + // Now start tracking and check whether the index status is correct. + $datasource = search_api_get_datasource_controller('node'); + $datasource->startTracking(array($index)); + $status = $datasource->getIndexStatus($index); + $this->assertEqual($status['total'], 3, 'Correct number of items marked for indexing on not bundle-specific index.'); + $datasource->stopTracking(array($index)); + + // Once again, but with only indexing articles. + $index->options['datasource']['bundles'] = array('article'); + drupal_static_reset('search_api_get_datasource_controller'); + $datasource = search_api_get_datasource_controller('node'); + $datasource->startTracking(array($index)); + $status = $datasource->getIndexStatus($index); + $this->assertEqual($status['total'], 2, 'Correct number of items marked for indexing on bundle-specific index.'); + $datasource->stopTracking(array($index)); + + // Now test that bundle renaming works. + $index->save(); + field_attach_rename_bundle('node', 'article', 'foo'); + $index = search_api_index_load('test2', TRUE); + $this->assertEqual($index->options['datasource']['bundles'], array('foo'), 'Bundle was correctly renamed in index settings.'); + $index->delete(); + } + } From 9f1f1ccfe02af607e1521a6fb889bce92c73e515 Mon Sep 17 00:00:00 2001 From: StryKaizer Date: Fri, 3 Apr 2015 14:15:55 +0200 Subject: [PATCH 144/278] Issue #2450227 by StryKaizer, drunken monkey: Fixed OR facets on taxonomy terms. --- CHANGELOG.txt | 1 + .../plugins/facetapi/query_type_term.inc | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 874aa279..5fcdf562 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2450227 by StryKaizer, drunken monkey: Fixed OR facets on taxonomy terms. - #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. - #1396222 by drunken monkey: Added a "First letter" aggregation type to the diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index b2c02c37..ddc119d0 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -54,6 +54,27 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy } elseif ($operator == FACETAPI_OPERATOR_OR) { $conjunction = 'OR'; + // When the operator is OR, remove parent terms from the active ones if + // children are active. If we don't do this, sending a term and its + // parent will produce the same results as just sending the parent. + if ($settings['flatten'] == '0') { + // Check the filters in reverse order, to avoid checking parents that + // will afterwards be removed anyways. + foreach (array_reverse(array_keys($active)) as $filter) { + // Skip this filter if it was already removed, or if it is the + // "missing value" filter ("!"). + if (!isset($active[$filter]) || !is_numeric($filter)) { + continue; + } + $parents = taxonomy_get_parents_all($filter); + // The return value of taxonomy_get_parents_all() includes the term + // itself at index 0. Remove that to only get the term's ancestors. + unset($parents[0]); + foreach ($parents as $parent) { + unset($active[$parent->tid]); + } + } + } } else { $vars = array( From 3d215013bb5a7d2a6edcd6621b834d5a5ad20986 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 23 May 2015 13:45:01 +0200 Subject: [PATCH 145/278] Issue #2450333 by drunken monkey: Added performance improvement when indexing entity references. --- CHANGELOG.txt | 2 ++ search_api.module | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5fcdf562..83bcc82b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2450333 by drunken monkey: Added performance improvement when indexing + entity references. - #2450227 by StryKaizer, drunken monkey: Fixed OR facets on taxonomy terms. - #1184610 by drunken monkey: Added option to limit indexes to specific entity bundles. diff --git a/search_api.module b/search_api.module index d145844d..b3301fe7 100644 --- a/search_api.module +++ b/search_api.module @@ -2256,12 +2256,13 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields } $property_info = $wrapper->$field->info(); $info['original_type'] = $property_info['type']; - // For entities, we extract the entity ID instead of the whole object. - // @todo Use 'identifier' => TRUE instead of always loading the object. + + // If the field value is an entity (or list of entities) but the field + // type is fulltext, we also add the label to the extracted value. $t = search_api_extract_inner_type($property_info['type']); - if (isset($entity_infos[$t])) { + if (isset($entity_infos[$t]) && search_api_is_text_type($info['type'])) { // If no object is set, set this field to NULL. - $info['value'] = $info['value'] ? _search_api_extract_entity_value($wrapper->$field, search_api_is_text_type($info['type'])) : NULL; + $info['value'] = $info['value'] ? _search_api_extract_entity_value($wrapper->$field, TRUE) : NULL; } } catch (EntityMetadataWrapperException $e) { From 5e330c1d1d7fcc19df2f8e3b64e32cf218251ade Mon Sep 17 00:00:00 2001 From: darrenoh Date: Sat, 23 May 2015 14:37:22 +0200 Subject: [PATCH 146/278] Issue #2414425 by Darren Oh, drunken monkey: Fixed backend form validation when adding or editing a server. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 83bcc82b..28bf2da5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2414425 by Darren Oh, drunken monkey: Fixed backend form validation when + adding or editing a server. - #2450333 by drunken monkey: Added performance improvement when indexing entity references. - #2450227 by StryKaizer, drunken monkey: Fixed OR facets on taxonomy terms. diff --git a/search_api.admin.inc b/search_api.admin.inc index 3cb9b4f3..a0d935c9 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -303,8 +303,9 @@ function search_api_admin_add_server_validate(array $form, array &$form_state) { return; } $form_state['values']['options']['service'] = $service; - $values = isset($form_state['values']['options']['form']) ? $form_state['values']['options']['form'] : array(); - $service->configurationFormValidate($form['options']['form'], $values, $form_state); + if (!empty($form_state['values']['options']['form'])) { + $service->configurationFormValidate($form['options']['form'], $form_state['values']['options']['form'], $form_state); + } } /** @@ -630,7 +631,9 @@ function search_api_admin_server_edit(array $form, array &$form_state, SearchApi * @see search_api_admin_server_edit_submit() */ function search_api_admin_server_edit_validate(array $form, array &$form_state) { - $form_state['server']->configurationFormValidate($form['options']['form'], $form_state['values']['options']['form'], $form_state); + if (!empty($form['options']['form']) && !empty($form_state['values']['options']['form'])) { + $form_state['server']->configurationFormValidate($form['options']['form'], $form_state['values']['options']['form'], $form_state); + } } /** From b151a4ad97bb24cb3e99dc70ffe697c334eba992 Mon Sep 17 00:00:00 2001 From: cgoffin Date: Sat, 23 May 2015 15:12:28 +0200 Subject: [PATCH 147/278] Issue #2448849 by cgoffin: Added "year range" option for date filters. --- CHANGELOG.txt | 1 + .../includes/handler_filter_date.inc | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 28bf2da5..43649d73 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2448849 by cgoffin: Added "year range" option for date filters. - #2414425 by Darren Oh, drunken monkey: Fixed backend form validation when adding or editing a server. - #2450333 by drunken monkey: Added performance improvement when indexing diff --git a/contrib/search_api_views/includes/handler_filter_date.inc b/contrib/search_api_views/includes/handler_filter_date.inc index f9812ba8..4b856dd8 100644 --- a/contrib/search_api_views/includes/handler_filter_date.inc +++ b/contrib/search_api_views/includes/handler_filter_date.inc @@ -16,6 +16,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { public function option_definition() { return parent::option_definition() + array( 'widget_type' => array('default' => 'default'), + 'year_range' => array('default' => '-3:+3'), ); } @@ -43,6 +44,24 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { '#options' => $widget_options, ); } + + if (module_exists('date_api')) { + $form['year_range'] = array( + '#type' => 'date_year_range', + '#default_value' => $this->options['year_range'], + ); + } + } + + /** + * Validate extra options. + */ + public function extra_options_validate($form, &$form_state) { + if (isset($form_state['values']['options']['year_range'])) { + if (!preg_match('/^(?:\-[0-9]{1,4}|[0-9]{4}):(?:[\+|\-][0-9]{1,4}|[0-9]{4})$/', $form_state['values']['options']['year_range'])) { + form_error($form['year_range'], t('Date year range must be in the format -9:+9, 2005:2010, -9:2010, or 2005:+9')); + } + } } /** @@ -56,6 +75,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) { $form['value']['#type'] = 'date_popup'; $form['value']['#date_format'] = 'm/d/Y'; + $form['value']['#date_year_range'] = $this->options['year_range']; unset($form['value']['#description']); } elseif (empty($form_state['exposed'])) { From 21e7d623d6e3fb7d913cc8c9031ffa2f52bb0dfd Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 27 May 2015 10:53:26 +0200 Subject: [PATCH 148/278] Follow-up to #2450333 by drunken monkey: Fixed indexing of entity-valued fields. --- search_api.module | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/search_api.module b/search_api.module index b3301fe7..6253221b 100644 --- a/search_api.module +++ b/search_api.module @@ -2212,6 +2212,9 @@ function search_api_index_update_datasource(SearchApiIndex $index, $table, $colu * The $fields array with additional "value" and "original_type" keys set. */ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields, array $value_options = array()) { + $value_options += array( + 'identifier' => TRUE, + ); // If $wrapper is a list of entities, we have to aggregate their field values. $wrapper_info = $wrapper->info(); if (search_api_is_list_type($wrapper_info['type'])) { @@ -2249,20 +2252,31 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields $info['original_type'] = $info['type']; if (isset($wrapper->$field)) { try { - $info['value'] = $wrapper->$field->value($value_options); - // For fulltext fields with options, also include the option labels. - if (search_api_is_text_type($info['type']) && $wrapper->$field->optionsList('view')) { - _search_api_add_option_values($info['value'], $wrapper->$field->optionsList('view')); - } + // Set the original type according to the field wrapper's info. $property_info = $wrapper->$field->info(); $info['original_type'] = $property_info['type']; - // If the field value is an entity (or list of entities) but the field - // type is fulltext, we also add the label to the extracted value. - $t = search_api_extract_inner_type($property_info['type']); - if (isset($entity_infos[$t]) && search_api_is_text_type($info['type'])) { - // If no object is set, set this field to NULL. - $info['value'] = $info['value'] ? _search_api_extract_entity_value($wrapper->$field, TRUE) : NULL; + // Extract the basic value from the field wrapper. + $info['value'] = $wrapper->$field->value($value_options); + + // For entities, we need to take care to differentiate between + // entities with ID 0 and empty fields. In the latter case, the + // wrapper's value() method returns, when called with "identifier = + // TRUE", FALSE instead of the (more logical) NULL. + $is_entity = isset($entity_infos[search_api_extract_inner_type($property_info['type'])]); + if ($is_entity && $info['value'] === FALSE) { + $info['value'] = NULL; + } + + // If we index the field as fulltext, we also include the entity label + // or option list label, if applicable. + if (search_api_is_text_type($info['type']) && isset($info['value'])) { + if ($wrapper->$field->optionsList('view')) { + _search_api_add_option_values($info['value'], $wrapper->$field->optionsList('view')); + } + elseif ($is_entity) { + $info['value'] = _search_api_extract_entity_value($wrapper->$field, TRUE); + } } } catch (EntityMetadataWrapperException $e) { From 21566e550ec37031f56345186bc00f066e7578b3 Mon Sep 17 00:00:00 2001 From: m1n0 Date: Mon, 1 Jun 2015 09:21:20 +0200 Subject: [PATCH 149/278] Issue #2190627 by m1n0, drunken monkey: Fixed fatal errors for views of disabled indexes. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 43649d73..3f6da725 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2190627 by m1n0, drunken monkey: Fixed fatal errors for views of disabled + indexes. - #2448849 by cgoffin: Added "year range" option for date filters. - #2414425 by Darren Oh, drunken monkey: Fixed backend form validation when adding or editing a server. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 1af1f089..82588a63 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -203,6 +203,10 @@ class SearchApiViewsQuery extends views_plugin_query { * Builds the necessary info to execute the query. */ public function build(&$view) { + if (!empty($this->errors)) { + return; + } + $this->view = $view; // Setup the nested filter structure for this query. From 90ba4e26b9aa36d698044509f5be4a483a7f72a4 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 3 Jun 2015 19:14:17 +0200 Subject: [PATCH 150/278] Adapted CHANGELOG.txt to 1.15 release. --- CHANGELOG.txt | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3f6da725..27b18df9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.15 (2015-06-03): +----------------------------- - #2190627 by m1n0, drunken monkey: Fixed fatal errors for views of disabled indexes. - #2448849 by cgoffin: Added "year range" option for date filters. @@ -17,7 +17,7 @@ Search API 1.x, dev (xx/xx/xxxx): Views. - #2387161 by drunken monkey: Added a hook for altering search results. -Search API 1.14 (12/26/2014): +Search API 1.14 (2014-12-26): ----------------------------- - #2382385 by illusionuk, drunken monkey: Fixed error handling when using invalid fulltext or sort field in Views. @@ -44,7 +44,7 @@ Search API 1.14 (12/26/2014): - #2278737 by drunken monkey: Fixed use of multiple Views fulltext search filters. -Search API 1.13 (07/23/2014): +Search API 1.13 (2014-07-23): ----------------------------- - #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache plugins. @@ -57,7 +57,7 @@ Search API 1.13 (07/23/2014): - #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string conversion in Highlighting processor. -Search API 1.12 (05/23/2014): +Search API 1.12 (2014-05-23): ----------------------------- - #2265349 by drunken monkey: Marked _search_api_settings_equals() as deprecated. @@ -84,7 +84,7 @@ Search API 1.12 (05/23/2014): - #2150779 by hefox: Fixed "Overridden" detection for index features. - #1227702 by drunken monkey: Improved error handling. -Search API 1.11 (12/25/2013): +Search API 1.11 (2013-12-25): ----------------------------- - #1879196 by drunken monkey: Fixed invalid old indexes causing errors. - #2155127 by drunken monkey: Clarified the scope of the "Node access" and @@ -100,7 +100,7 @@ Search API 1.11 (12/25/2013): - #2146435 by timkang: Fixed Views paging with custom pager add-ons. - #2150347 by drunken monkey: Added access callbacks for indexes and servers. -Search API 1.10 (12/09/2013): +Search API 1.10 (2013-12-09): ----------------------------- - #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs. - #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c. @@ -141,7 +141,7 @@ Search API 1.10 (12/09/2013): - #2100191 by drunken monkey, Bojhan: Added an admin description to the Search API landing page. -Search API 1.9 (10/23/2013): +Search API 1.9 (2013-10-23): ---------------------------- - #2113277 by moonray, drunken monkey: Fixed date facet count for active item. - #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields. @@ -170,7 +170,7 @@ Search API 1.9 (10/23/2013): - #2075839 by leeomara, drunken monkey: Added descriptions to field lists for 'Aggregated Fields'. -Search API 1.8 (09/01/2013): +Search API 1.8 (2013-09-01): ---------------------------- - #1414048 by drunken monkey: Fixed exception in views.inc removes all Search API tables. @@ -204,7 +204,7 @@ Search API 1.8 (09/01/2013): - #2040111 by arpieb: Fixed Views URL argument handler to allow multiple values. - #1064520 by drunken monkey: Added a processor for highlighting. -Search API 1.7 (07/01/2013): +Search API 1.7 (2013-07-01): ---------------------------- - #1612708 by drunken monkey: Fixed Views caching with facet blocks. - #2024189 by drunken monkey: Improved serialization of the query class. @@ -231,7 +231,7 @@ Search API 1.7 (07/01/2013): - #1285794 by drunken monkey: Fixed "All" option in Views' exposed "Items per page" setting. -Search API 1.6 (05/29/2013): +Search API 1.6 (2013-05-29): ---------------------------- - #1649976 by Berdir, ilari.stenroth, drunken monkey: Fixed memory error during crons run for large indexes. @@ -249,7 +249,7 @@ Search API 1.6 (05/29/2013): - #1760706 by jgraham, das-peter, drunken monkey: Added a flexible way for determining whether an index contains entities. -Search API 1.5 (05/04/2013): +Search API 1.5 (2013-05-04): ---------------------------- - #1169254 by cslavoie, drunken monkey, DYdave: Added transliteration processor. - #1959088 by drunken monkey: Fixed titles for contextual filters. @@ -266,7 +266,7 @@ Search API 1.5 (05/04/2013): in the Hierarchy data alteration. - #1702604 by JvE, slucero: Added option for maximum date facet depth. -Search API 1.4 (01/09/2013): +Search API 1.4 (2013-01-09): ---------------------------- - #1827272 by drunken monkey: Fixed regression introduced by #1777710. - #1807622 by drunken monkey: Fixed definition of the default node index. @@ -276,7 +276,7 @@ Search API 1.4 (01/09/2013): filters. - #1823916 by aschiwi: Fixed batch_sise typos. -Search API 1.3 (10/10/2012): +Search API 1.3 (2012-10-10): ---------------------------- - Patch by mr.baileys: Fixed "enable" function doesn't use security tokens. - #1318904 by becw, das-peter, orakili, drunken monkey: Added improved handling @@ -292,7 +292,7 @@ Search API 1.3 (10/10/2012): - #1414138 by drunken monkey: Fixed internal static index property cache. - #1253320 by drunken monkey, fago: Fixed improper error handling. -Search API 1.2 (07/07/2012): +Search API 1.2 (2012-07-07): ---------------------------- - #1368548 by das-peter: Do not index views results by entity id. - #1422750 by drunken monkey, sepgil: Fixed illegal modification of entity @@ -305,7 +305,7 @@ Search API 1.2 (07/07/2012): changed. - #1528436 by jsacksick, drunken monkey: Fixed handling of exportable entities. -Search API 1.1 (05/23/2012): +Search API 1.1 (2012-05-23): ---------------------------- - Fixed escaping of error messages. - #1330506 by drunken monkey: Removed the old Facets module. @@ -316,7 +316,7 @@ Search API 1.1 (05/23/2012): $service->configurationFormValidate() for empty forms. - #1400882 by mh86: Fixed "Index hierarchy" for "All parents". -Search API 1.0 (12/15/2011): +Search API 1.0 (2011-12-15): ---------------------------- - #1350322 by drunken monkey: Fixed regressions introduced with cron queue indexing. @@ -334,7 +334,7 @@ Search API 1.0 (12/15/2011): dependency plugin. - #1337292 by drunken monkey: Fixed facet dependency system. -Search API 1.0, RC 1 (11/10/2011): +Search API 1.0, RC 1 (2011-11-10): ---------------------------------- API changes: - #1260834 by drunken monkey: Added a way to define custom data types. @@ -405,7 +405,7 @@ Others: - #1161532 by drunken monkey: Fixed discerning between delete and revert in hook_*_delete(). -Search API 1.0, Beta 10 (06/20/2011): +Search API 1.0, Beta 10 (2011-06-20): ------------------------------------- API changes: - #1068342 by drunken monkey: Added a 'fields to run on' option for processors. @@ -419,7 +419,7 @@ Others: - #1133864 by agentrickard, awolfey, greg.1.anderson, drunken monkey: Added Drush integration. -Search API 1.0, Beta 9 (06/06/2011): +Search API 1.0, Beta 9 (2011-06-06): ------------------------------------ API changes: - #1089758 by becw, drunken monkey: Updated Views field handlers to utilize new @@ -461,7 +461,7 @@ Others: - #1120850 by drunken monkey, fangel: Fixed type of related entities in nested lists. -Search API 1.0, Beta 8 (04/02/2011): +Search API 1.0, Beta 8 (2011-04-02): ------------------------------------ API changes: - #1012878 by drunken monkey: Added a way to index an entity directly. @@ -480,12 +480,12 @@ Others: search_api_facets_by_block_status(). - #1081666 by danielnolde: Fixed PHP notices when property labels are missing. -Search API 1.0, Beta 7 (03/08/2011): +Search API 1.0, Beta 7 (2011-03-08): ------------------------------------ - #1083828 by drunken monkey: Added documentation on indexing custom data. - #1081244 by drunken monkey: Fixed debug line still contained in DB backend. -Search API 1.0, Beta 6 (03/04/2011): +Search API 1.0, Beta 6 (2011-03-04): ------------------------------------ API changes: - #1075810 by drunken monkey: Added API function for marking entities as dirty. @@ -528,7 +528,7 @@ Others: - #1024514: Error when preprocessing muli-valued fulltext fields. - #1020372: CSS classes for facets. -Search API 1.0, Beta 5 (01/05/2011): +Search API 1.0, Beta 5 (2011-01-05): ------------------------------------ API changes: - #917998: Enhance data alterations by making them objects. @@ -550,7 +550,7 @@ Others: - #985324: Add "Current search" block. - #984174: Bug in Index::prepareProcessors() when processors have not been set. -Search API 1.0, Beta 4 (11/29/2010): +Search API 1.0, Beta 4 (2010-11-29): ------------------------------------ API changes: - #976876: Move Solr module into its own project. @@ -592,7 +592,7 @@ Others: - #938982: Not all SearchApiQuery options are passed. - #931066 by luke_b: HTTP timeout not set correctly. -Search API 1.0, Beta 3 (09/30/2010): +Search API 1.0, Beta 3 (2010-09-30): ------------------------------------ - API mostly stable. - Five contrib modules exist: From 8dbd9513a01872378a2cc10e0891a9c9517029bc Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 3 Jun 2015 19:14:40 +0200 Subject: [PATCH 151/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 27b18df9..59329622 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.15 (2015-06-03): ----------------------------- - #2190627 by m1n0, drunken monkey: Fixed fatal errors for views of disabled From d8dab1af68a19e707871810edf5b4d1d242d5947 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 8 Jun 2015 08:24:50 +0200 Subject: [PATCH 152/278] Issue #2447213 by drunken monkey: Fixed issues with stale field settings for MLT contextual filter. --- CHANGELOG.txt | 2 ++ .../includes/handler_argument_more_like_this.inc | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 59329622..90e1f4bc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2447213 by drunken monkey: Fixed issues with stale field settings for MLT + contextual filter. Search API 1.15 (2015-06-03): ----------------------------- diff --git a/contrib/search_api_views/includes/handler_argument_more_like_this.inc b/contrib/search_api_views/includes/handler_argument_more_like_this.inc index 69e4a54c..9708851c 100644 --- a/contrib/search_api_views/includes/handler_argument_more_like_this.inc +++ b/contrib/search_api_views/includes/handler_argument_more_like_this.inc @@ -71,11 +71,12 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg $this->query->abort(); return; } - $fields = $this->options['fields'] ? $this->options['fields'] : array(); - if (empty($fields)) { - foreach ($this->query->getIndex()->options['fields'] as $key => $field) { - $fields[] = $key; - } + $index_fields = array_keys($this->query->getIndex()->options['fields']); + if (empty($this->options['fields'])) { + $fields = $index_fields; + } + else { + $fields = array_intersect($this->options['fields'], $index_fields); } $mlt = array( 'id' => $this->argument, From 5990977545d2b60f00cc64b3e6ff767a12fe329a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 8 Jun 2015 17:33:20 +0200 Subject: [PATCH 153/278] Fixed a small coding mistake. --- includes/datasource_entity.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index ddfd34ac..59f5bd93 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -229,7 +229,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon '#attributes' => array('class' => array('search-api-checkboxes-list')), '#disabled' => !empty($form_state['index']), ); - if (!empty($form_state['index'])) { + if (!empty($form_state['index']->options['datasource'])) { $form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']); } return $form; From 8dc293b6bff479202cee132e68e2c8b035d5c7e5 Mon Sep 17 00:00:00 2001 From: dww Date: Tue, 30 Jun 2015 13:53:17 +0200 Subject: [PATCH 154/278] Issue #2489882 by dww: Fixed Views taxonomy filter with "is (not) empty" operator --- CHANGELOG.txt | 1 + .../includes/handler_filter_taxonomy_term.inc | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 90e1f4bc..da939d42 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2489882 by dww: Fixed Views taxonomy filter with "is (not) empty" operator - #2447213 by drunken monkey: Fixed issues with stale field settings for MLT contextual filter. diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index 02d30686..efa685af 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -178,12 +178,25 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte return TRUE; } + // We need to know the operator, which is normally set in + // views_handler_filter::accept_exposed_input(), before we actually call + // the parent version of ourselves. + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) { + $this->operator = $input[$this->options['expose']['operator_id']]; + } + // If view is an attachment and is inheriting exposed filters, then assume // exposed input has already been validated. if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) { $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; } + // If we're checking for EMPTY or NOT, we don't need any input, and we can + // say that our input conditions are met by just having the right operator. + if ($this->operator == 'empty' || $this->operator == 'not empty') { + return TRUE; + } + // If it's non-required and there's no value don't bother filtering. if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) { return FALSE; From 834d1d16f2bf8cd868ff88af41f6ad4f4dd2bcf2 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 16 Jul 2015 14:53:04 +0200 Subject: [PATCH 155/278] Issue #2520684 by drunken monkey: Fixed "bundles" setting on indexes with "Index immediately". --- CHANGELOG.txt | 2 ++ includes/datasource.inc | 58 +++++++++++++++++++++++++++-------------- search_api.module | 15 ++++++++--- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index da939d42..16a259f7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2520684 by drunken monkey: Fixed "bundles" setting on indexes with "Index + immediately". - #2489882 by dww: Fixed Views taxonomy filter with "is (not) empty" operator - #2447213 by drunken monkey: Fixed issues with stale field settings for MLT contextual filter. diff --git a/includes/datasource.inc b/includes/datasource.inc index bac2a658..0c72f014 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -183,6 +183,10 @@ interface SearchApiDataSourceControllerInterface { * The concept of queued items will be removed in the Drupal 8 version of * this module. * + * @return SearchApiIndex[]|null + * All indexes for which any items were updated; or NULL if items were + * updated for all of them. + * * @throws SearchApiDataSourceException * If any error state was encountered. */ @@ -231,6 +235,10 @@ interface SearchApiDataSourceControllerInterface { * @param SearchApiIndex[] $indexes * The indexes for which the deletions should be tracked. * + * @return SearchApiIndex[]|null + * All indexes for which any items were deleted; or NULL if items were + * deleted for all of them. + * * @throws SearchApiDataSourceException * If any error state was encountered. */ @@ -615,23 +623,28 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) { if (!$this->table) { - return; + return NULL; } - $index_ids = array(); + + $ret = array(); + foreach ($indexes as $index) { $this->checkIndex($index); - $index_ids[] = $index->id; - } - $update = db_update($this->table) - ->fields(array( - $this->changedColumn => REQUEST_TIME, - )) - ->condition($this->indexIdColumn, $index_ids, 'IN') - ->condition($this->changedColumn, 0, $dequeue ? '<=' : '='); - if ($item_ids !== FALSE) { - $update->condition($this->itemIdColumn, $item_ids, 'IN'); + $update = db_update($this->table) + ->fields(array( + $this->changedColumn => REQUEST_TIME, + )) + ->condition($this->indexIdColumn, $index->id) + ->condition($this->changedColumn, 0, $dequeue ? '<=' : '='); + if ($item_ids !== FALSE) { + $update->condition($this->itemIdColumn, $item_ids, 'IN'); + } + if ($update->execute()) { + $ret[] = $index; + } } - $update->execute(); + + return $ret; } /** @@ -675,17 +688,22 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou */ public function trackItemDelete(array $item_ids, array $indexes) { if (!$this->table) { - return; + return NULL; } - $index_ids = array(); + + $ret = array(); + foreach ($indexes as $index) { $this->checkIndex($index); - $index_ids[] = $index->id; + $delete = db_delete($this->table) + ->condition($this->indexIdColumn, $index->id) + ->condition($this->itemIdColumn, $item_ids, 'IN'); + if ($delete->execute()) { + $ret[] = $index; + } } - db_delete($this->table) - ->condition($this->itemIdColumn, $item_ids, 'IN') - ->condition($this->indexIdColumn, $index_ids, 'IN') - ->execute(); + + return $ret; } /** diff --git a/search_api.module b/search_api.module index 6253221b..86b09189 100644 --- a/search_api.module +++ b/search_api.module @@ -1172,7 +1172,10 @@ function search_api_track_item_change($type, array $item_ids) { return; } try { - search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes); + $returned_indexes = search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes); + if (isset($returned_indexes)) { + $indexes = $returned_indexes; + } foreach ($indexes as $index) { if (!empty($index->options['index_directly'])) { // For indexes with the index_directly option set, queue the items to be @@ -1247,7 +1250,10 @@ function search_api_track_item_delete($type, array $item_ids) { $indexes = search_api_index_load_multiple(FALSE, $conditions); if ($indexes) { try { - search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes); + $changed_indexes = search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes); + if (isset($changed_indexes)) { + $indexes = $changed_indexes; + } } catch (SearchApiException $e) { $vars['%item_type'] = $type; @@ -1257,8 +1263,9 @@ function search_api_track_item_delete($type, array $item_ids) { // Then, delete it from all servers. Servers of disabled indexes have to be // considered, too! - unset($conditions['enabled']); - foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { + $conditions['enabled'] = 0; + $indexes = array_merge($indexes, search_api_index_load_multiple(FALSE, $conditions)); + foreach ($indexes as $index) { try { if ($server = $index->server()) { $server->deleteItems($item_ids, $index); From eb4924d51d7fd8e0fc08ac20b6d244ec210427ee Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 5 Aug 2015 15:55:14 +0200 Subject: [PATCH 156/278] Small code style fix. --- includes/index_entity.inc | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 9fc1f68b..824bf129 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -439,7 +439,6 @@ class SearchApiIndex extends Entity { return $this->server()->query($this, $options); } - /** * Indexes items on this index. * From 2ac29dc3febcac708e7e7cce48c8ac60b8a61eb9 Mon Sep 17 00:00:00 2001 From: prics Date: Thu, 6 Aug 2015 18:44:31 +0200 Subject: [PATCH 157/278] Issue #2479453 by prics, drunken monkey: Added a Drush command to list/enable/disable servers. --- CHANGELOG.txt | 2 + search_api.drush.inc | 108 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 16a259f7..b1327af4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2479453 by prics, drunken monkey: Added a Drush command to + list/enable/disable servers. - #2520684 by drunken monkey: Fixed "bundles" setting on indexes with "Index immediately". - #2489882 by dww: Fixed Views taxonomy filter with "is (not) empty" operator diff --git a/search_api.drush.inc b/search_api.drush.inc index 927514d0..9edefe94 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -121,6 +121,39 @@ function search_api_drush_command() { 'aliases' => array('sapi-sis'), ); + $items['search-api-server-list'] = array( + 'description' => 'List all search servers.', + 'examples' => array( + 'drush search-api-server-list' => dt('List all search servers.'), + 'drush sapi-sl' => dt('Alias to list all search servers.'), + ), + 'aliases' => array('sapi-sl'), + ); + + $items['search-api-server-enable'] = array( + 'description' => 'Enable a search server.', + 'examples' => array( + 'drush search-api-server-e my_solr_server' => dt('Enable the !server search server.', array('!server' => 'my_solr_server')), + 'drush sapi-se my_solr_server' => dt('Alias to enable the !server search server.', array('!server' => 'my_solr_server')), + ), + 'arguments' => array( + 'server_id' => dt('The numeric ID or machine name of a search server to enable.'), + ), + 'aliases' => array('sapi-se'), + ); + + $items['search-api-server-disable'] = array( + 'description' => 'Disable a search server.', + 'examples' => array( + 'drush search-api-server-disable' => dt('Disable the !server search server.', array('!server' => 'my_solr_server')), + 'drush sapi-sd' => dt('Alias to disable the !server search server.', array('!server' => 'my_solr_server')), + ), + 'arguments' => array( + 'server_id' => dt('The numeric ID or machine name of a search server to disable.'), + ), + 'aliases' => array('sapi-sd'), + ); + return $items; } @@ -508,7 +541,8 @@ function search_api_drush_get_server($server_id = NULL) { $servers = search_api_server_load_multiple($ids); if (empty($servers)) { drush_set_error(dt('Invalid server_id or no servers present.')); - // @todo: Maybe add logic to print table of all servers. + drush_print(); + drush_search_api_server_list(); } return $servers; } @@ -533,3 +567,75 @@ function search_api_drush_static($function) { $index[$function] = TRUE; return FALSE; } + +/** + * Lists all search servers. + */ +function drush_search_api_server_list() { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + $servers = search_api_server_load_multiple(FALSE); + if (empty($servers)) { + drush_print(dt('There are no servers present.')); + return; + } + $rows[] = array( + dt('Machine name'), + dt('Name'), + dt('Status'), + ); + foreach ($servers as $server) { + $row = array( + $server->machine_name, + $server->name, + $server->enabled ? dt('enabled') : dt('disabled'), + ); + $rows[] = $row; + } + drush_print_table($rows); +} + +/** + * Enables a search server. + * + * @param int|string $server_id + * The numeric ID or machine name of the server to enable. + */ +function drush_search_api_server_enable($server_id = NULL) { + if (!isset($server_id)) { + drush_print(dt('Please provide a valid server id.')); + return; + } + $server = search_api_server_load($server_id); + if (empty($server)) { + drush_print(dt('The server was not able to load.')); + return; + } + else { + $server->update(array('enabled' => 1)); + drush_print(dt('The server was enabled successfully.')); + } +} + +/** + * Disables a search server. + * + * @param int|string $server_id + * The numeric ID or machine name of the server to disable. + */ +function drush_search_api_server_disable($server_id = NULL) { + if (!isset($server_id)) { + drush_print(dt('Please provide a valid server id.')); + return; + } + $server = search_api_server_load($server_id); + if (empty($server)) { + drush_print(dt('The server was not able to load.')); + return; + } + else { + $server->update(array('enabled' => 0)); + drush_print(dt('The server was disabled successfully.')); + } +} From 0cc0f807fc338d0391ab9d14a150c01a59782db9 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 7 Aug 2015 22:05:26 +0200 Subject: [PATCH 158/278] Issue #2533096 by drunken monkey: Fixed uncaught exception when deleting a server. --- CHANGELOG.txt | 1 + includes/service.inc | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b1327af4..48bffd82 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2533096 by drunken monkey: Fixed uncaught exception when deleting a server. - #2479453 by prics, drunken monkey: Added a Drush command to list/enable/disable servers. - #2520684 by drunken monkey: Fixed "bundles" setting on indexes with "Index diff --git a/includes/service.inc b/includes/service.inc index b8d8ca88..046e5d39 100644 --- a/includes/service.inc +++ b/includes/service.inc @@ -420,7 +420,15 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { public function preDelete() { $indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name)); foreach ($indexes as $index) { - $this->removeIndex($index); + // removeIndex() might throw exceptions, but this method mustn't. + try { + $this->removeIndex($index); + } + catch (SearchApiException $e) { + $variables['%index'] = $index->name; + $variables['%server'] = $this->server->name; + watchdog_exception('search_api', $e, '%type while trying to remove index %index from deleted server %server: !message in %function (line %line of %file).', $variables); + } } } From f062a8b4538c07b6e74225e02422cd81f8769ec6 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 7 Aug 2015 22:08:02 +0200 Subject: [PATCH 159/278] Issue #2520934 by drunken monkey: Added an item type for indexing several types of entities in one index. --- CHANGELOG.txt | 2 + contrib/search_api_views/includes/query.inc | 22 +- includes/callback_bundle_filter.inc | 90 +++-- includes/datasource_multiple.inc | 357 ++++++++++++++++++++ search_api.info | 1 + search_api.install | 64 ++++ search_api.module | 88 ++++- 7 files changed, 582 insertions(+), 42 deletions(-) create mode 100644 includes/datasource_multiple.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 48bffd82..468bed37 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2520934 by drunken monkey: Added an item type for indexing several types of + entities in one index. - #2533096 by drunken monkey: Fixed uncaught exception when deleting a server. - #2479453 by prics, drunken monkey: Added a Drush command to list/enable/disable servers. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 82588a63..ee551bba 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -494,31 +494,31 @@ class SearchApiViewsQuery extends views_plugin_query { * query backend. */ public function get_result_wrappers($results, $relationship = NULL, $field = NULL) { - $entity_type = $this->index->getEntityType(); + $type = $this->index->item_type; $wrappers = array(); - $load_entities = array(); + $load_items = array(); foreach ($results as $row_index => $row) { - if ($entity_type && isset($row->entity)) { + if (isset($row->entity)) { // If this entity isn't load, register it for pre-loading. if (!is_object($row->entity)) { - $load_entities[$row->entity] = $row_index; + $load_items[$row->entity] = $row_index; + } + else { + $wrappers[$row_index] = $this->index->entityWrapper($row->entity); } - - $wrappers[$row_index] = $this->index->entityWrapper($row->entity); } } // If the results are entities, we pre-load them to make use of a multiple // load. (Otherwise, each result would be loaded individually.) - if (!empty($load_entities)) { - $entities = entity_load($entity_type, array_keys($load_entities)); - foreach ($entities as $entity_id => $entity) { - $wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity); + if (!empty($load_items)) { + $items = $this->index->loadItems(array_keys($load_items)); + foreach ($items as $id => $item) { + $wrappers[$load_items[$id]] = $this->index->entityWrapper($item); } } // Apply the relationship, if necessary. - $type = $entity_type ? $entity_type : $this->index->item_type; $selector_suffix = ''; if ($field && ($pos = strrpos($field, ':'))) { $selector_suffix = substr($field, 0, $pos); diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index 08fcba45..50a9dc9f 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -14,24 +14,40 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * {@inheritdoc} */ public function supportsIndex(SearchApiIndex $index) { - $support = $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); - $support &= empty($index->options['datasource']['bundles']) || count($index->options['datasource']['bundles']) > 1; - return $support; + if ($this->isMultiEntityIndex($index)) { + $info = entity_get_info(); + foreach ($index->options['datasource']['types'] as $type) { + if (isset($info[$type]) && self::hasBundles($info[$type])) { + return TRUE; + } + } + return FALSE; + } + return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info); } /** * {@inheritdoc} */ public function alterItems(array &$items) { - $info = entity_get_info($this->index->getEntityType()); - if (self::hasBundles($info) && isset($this->options['bundles'])) { - $bundles = array_flip($this->options['bundles']); - $default = (bool) $this->options['default']; + if (!$this->supportsIndex($this->index) || !isset($this->options['bundles'])) { + return; + } + + if ($this->isMultiEntityIndex()) { + $bundle_prop = 'item_bundle'; + } + else { + $info = entity_get_info($this->index->getEntityType()); $bundle_prop = $info['entity keys']['bundle']; - foreach ($items as $id => $item) { - if (isset($bundles[$item->$bundle_prop]) == $default) { - unset($items[$id]); - } + } + + $bundles = array_flip($this->options['bundles']); + $default = (bool) $this->options['default']; + + foreach ($items as $id => $item) { + if (isset($bundles[$item->$bundle_prop]) == $default) { + unset($items[$id]); } } } @@ -40,20 +56,35 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * {@inheritdoc} */ public function configurationForm() { - $info = entity_get_info($this->index->getEntityType()); - if (self::hasBundles($info)) { + if ($this->supportsIndex($this->index)) { $options = array(); - foreach ($info['bundles'] as $bundle => $bundle_info) { - $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; + if ($this->isMultiEntityIndex()) { + $info = entity_get_info(); + $unsupported_types = array(); + foreach ($this->index->options['datasource']['types'] as $type) { + if (isset($info[$type]) && self::hasBundles($info[$type])) { + foreach ($info[$type]['bundles'] as $bundle => $bundle_info) { + $options["$type:$bundle"] = $info[$type]['label'] . ' » ' . $bundle_info['label']; + } + } + else { + $unsupported_types[] = isset($info[$type]['label']) ? $info[$type]['label'] : $type; + } + } + if ($unsupported_types) { + $form['unsupported_types']['#markup'] = '

    ' . t('The following entity types do not contain any bundles: @types. All items of those types will therefore be included in the index.', array('@types' => implode(', ', $unsupported_types))) . '

    '; + } + } + else { + $info = entity_get_info($this->index->getEntityType()); + foreach ($info['bundles'] as $bundle => $bundle_info) { + $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle; + } } if (!empty($this->index->options['datasource']['bundles'])) { $form['message']['#markup'] = '

    ' . t("Note: This index is already restricted to certain bundles. If you use this data alteration, those will be reduced further. However, the index setting is better supported in the user interface and should therefore be prefered. For example, using this data alteration will not reduce the displayed total number of items to index (even though some of them will not be indexed). Consider creating a new index with appropriate bundle settings instead.") . '

    '; - $included_bundles = drupal_map_assoc($this->index->options['datasource']['bundles']); - foreach ($options as $bundle => $label) { - if (!isset($included_bundles[$bundle])) { - unset($options[$bundle]); - } - } + $included_bundles = array_flip($this->index->options['datasource']['bundles']); + $options = array_intersect_key($options, $included_bundles); } $form['default'] = array( '#type' => 'radios', @@ -90,10 +121,25 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { * The entity type's entity_get_info() array. * * @return bool - * TRUE if the entity type has bundles, FASLE otherwise. + * TRUE if the entity type has bundles, FALSE otherwise. */ protected static function hasBundles(array $entity_info) { return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']); } + /** + * Determines whether the given index contains multiple types of entities. + * + * @param SearchApiIndex|null $index + * (optional) The index to examine. Defaults to the index set for this + * plugin. + * + * @return bool + * TRUE if the index is a multi-entity index, FALSE otherwise. + */ + protected function isMultiEntityIndex(SearchApiIndex $index = NULL) { + $index = $index ? $index : $this->index; + return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController; + } + } diff --git a/includes/datasource_multiple.inc b/includes/datasource_multiple.inc new file mode 100644 index 00000000..75adf459 --- /dev/null +++ b/includes/datasource_multiple.inc @@ -0,0 +1,357 @@ + 'item_id', + 'type' => 'string', + ); + } + + /** + * {@inheritdoc} + */ + public function loadItems(array $ids) { + $ids_by_type = array(); + foreach ($ids as $id) { + list($type, $entity_id) = explode('/', $id); + $ids_by_type[$type][$entity_id] = $id; + } + + $items = array(); + foreach ($ids_by_type as $type => $type_ids) { + foreach (entity_load($type, array_keys($type_ids)) as $entity_id => $entity) { + $id = $type_ids[$entity_id]; + $item = (object) array($type => $entity); + $item->item_id = $id; + $item->item_type = $type; + $item->item_entity_id = $entity_id; + $item->item_bundle = NULL; + try { + list(, , $bundle) = entity_extract_ids($type, $entity); + $item->item_bundle = $bundle ? "$type:$bundle" : NULL; + } + catch (EntityMalformedException $e) { + // Will probably make problems at some other place, but for extracting + // the bundle it is really not critical enough to fail on – just + // ignore this exception. + } + $items[$id] = $item; + unset($type_ids[$entity_id]); + } + if ($type_ids) { + search_api_track_item_delete($type, array_keys($type_ids)); + } + } + + return $items; + } + + /** + * {@inheritdoc} + */ + protected function getPropertyInfo() { + $info = array( + 'item_id' => array( + 'label' => t('ID'), + 'description' => t('The combined ID of the item, containing both entity type and entity ID.'), + 'type' => 'token', + ), + 'item_type' => array( + 'label' => t('Entity type'), + 'description' => t('The entity type of the item.'), + 'type' => 'token', + 'options list' => 'search_api_entity_type_options_list', + ), + 'item_entity_id' => array( + 'label' => t('Entity ID'), + 'description' => t('The entity ID of the item.'), + 'type' => 'token', + ), + 'item_bundle' => array( + 'label' => t('Bundle'), + 'description' => t('The bundle of the item, if applicable.'), + 'type' => 'token', + 'options list' => 'search_api_combined_bundle_options_list', + ), + 'item_label' => array( + 'label' => t('Label'), + 'description' => t('The label of the item.'), + 'type' => 'text', + // Since this needs a bit more computation than the others, we don't + // include it always when loading the item but use a getter callback. + 'getter callback' => 'search_api_get_multi_type_item_label', + ), + ); + + foreach ($this->getSelectedEntityTypeOptions() as $type => $label) { + $info[$type] = array( + 'label' => $label, + 'description' => t('The indexed entity, if it is of type %type.', array('%type' => $label)), + 'type' => $type, + ); + } + + return array('property info' => $info); + } + + /** + * {@inheritdoc} + */ + public function getItemId($item) { + return isset($item->item_id) ? $item->item_id : NULL; + } + + /** + * {@inheritdoc} + */ + public function getItemLabel($item) { + return search_api_get_multi_type_item_label($item); + } + + /** + * {@inheritdoc} + */ + public function getItemUrl($item) { + if ($item->item_type == 'file') { + return array( + 'path' => file_create_url($item->file->uri), + 'options' => array( + 'entity_type' => 'file', + 'entity' => $item, + ), + ); + } + $url = entity_uri($item->item_type, $item->{$item->item_type}); + return $url ? $url : NULL; + } + + /** + * {@inheritdoc} + */ + public function startTracking(array $indexes) { + if (!$this->table) { + return; + } + // We first clear the tracking table for all indexes, so we can just insert + // all items again without any key conflicts. + $this->stopTracking($indexes); + + foreach ($indexes as $index) { + $types = $this->getEntityTypes($index); + + // Wherever possible, use a sub-select instead of the much slower + // entity_load(). + foreach ($types as $type) { + $entity_info = entity_get_info($type); + + if (!empty($entity_info['base table'])) { + // Assumes that all entities use the "base table" property and the + // "entity keys[id]" in the same way as the default controller. + $id_field = $entity_info['entity keys']['id']; + $table = $entity_info['base table']; + + // Select all entity ids. + $query = db_select($table, 't'); + $query->addExpression("CONCAT(:prefix, t.$id_field)", 'item_id', array(':prefix' => $type . '/')); + $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id)); + $query->addExpression('1', 'changed'); + + // INSERT ... SELECT ... + db_insert($this->table) + ->from($query) + ->execute(); + + unset($types[$type]); + } + } + + // In the absence of a "base table", use the slow entity_load(). + if ($types) { + foreach ($types as $type) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', $type); + $result = $query->execute(); + $ids = !empty($result[$type]) ? array_keys($result[$type]) : array(); + if ($ids) { + foreach ($ids as $i => $id) { + $ids[$i] = $type . '/' . $id; + } + $this->trackItemInsert($ids, array($index), TRUE); + } + } + } + } + } + + /** + * Starts tracking the index status for the given items on the given indexes. + * + * @param array $item_ids + * The IDs of new items to track. + * @param SearchApiIndex[] $indexes + * The indexes for which items should be tracked. + * @param bool $skip_type_check + * (optional) If TRUE, don't check whether the type matches the index's + * datasource configuration. Internal use only. + * + * @return SearchApiIndex[]|null + * All indexes for which any items were added; or NULL if items were added + * for all of them. + * + * @throws SearchApiDataSourceException + * If any error state was encountered. + */ + public function trackItemInsert(array $item_ids, array $indexes, $skip_type_check = FALSE) { + $ret = array(); + + foreach ($indexes as $index_id => $index) { + $ids = drupal_map_assoc($item_ids); + + if (!$skip_type_check) { + $types = $this->getEntityTypes($index); + foreach ($ids as $id) { + list($type) = explode('/', $id); + if (!isset($types[$type])) { + unset($ids[$id]); + } + } + } + + if ($ids) { + parent::trackItemInsert($ids, array($index)); + $ret[$index_id] = $index; + } + } + + return $ret; + } + + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + $form['types'] = array( + '#type' => 'checkboxes', + '#title' => t('Entity types'), + '#description' => t('Select the entity types which should be included in this index.'), + '#options' => search_api_entity_type_options_list(), + '#attributes' => array('class' => array('search-api-checkboxes-list')), + '#disabled' => !empty($form_state['index']), + '#required' => TRUE, + ); + if (!empty($form_state['index']->options['datasource']['types'])) { + $form['types']['#default_value'] = $this->getEntityTypes($form_state['index']); + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + if (!empty($values['types'])) { + $values['types'] = array_keys(array_filter($values['types'])); + } + } + + /** + * {@inheritdoc} + */ + public function getConfigurationSummary(SearchApiIndex $index) { + if ($type_labels = $this->getSelectedEntityTypeOptions($index)) { + $args['!types'] = implode(', ', $type_labels); + return format_plural(count($type_labels), 'Indexed entity types: !types.', 'Indexed entity types: !types.', $args); + } + return NULL; + } + + /** + * Retrieves the index for which the current method was called. + * + * Very ugly method which uses the stack trace to find the right object. + * + * @return SearchApiIndex + * The active index. + * + * @throws SearchApiException + * Thrown if the active index could not be determined. + */ + protected function getCallingIndex() { + foreach (debug_backtrace() as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof SearchApiIndex) { + return $trace['object']; + } + } + // If there's only a single index on the site, it's also easy. + $indexes = search_api_index_load_multiple(FALSE); + if (count($indexes) === 1) { + return reset($indexes); + } + throw new SearchApiException('Could not determine the active index of the datasource.'); + } + + /** + * Returns the entity types for which this datasource is configured. + * + * Depends on the index from which this method is (indirectly) called. + * + * @param SearchApiIndex $index + * (optional) The index for which to get the enabled entity types. If not + * given, will be determined automatically. + * + * @return string[] + * The machine names of the datasource's enabled entity types, as both keys + * and values. + * + * @throws SearchApiException + * Thrown if the active index could not be determined. + */ + protected function getEntityTypes(SearchApiIndex $index = NULL) { + if (!$index) { + $index = $this->getCallingIndex(); + } + if (isset($index->options['datasource']['types'])) { + return drupal_map_assoc($index->options['datasource']['types']); + } + return array(); + } + + /** + * Returns the selected entity type options for this datasource. + * + * Depends on the index from which this method is (indirectly) called. + * + * @param SearchApiIndex $index + * (optional) The index for which to get the enabled entity types. If not + * given, will be determined automatically. + * + * @return string[] + * An associative array, mapping the machine names of the enabled entity + * types to their labels. + * + * @throws SearchApiException + * Thrown if the active index could not be determined. + */ + protected function getSelectedEntityTypeOptions(SearchApiIndex $index = NULL) { + return array_intersect_key(search_api_entity_type_options_list(), $this->getEntityTypes($index)); + } + +} diff --git a/search_api.info b/search_api.info index 5b65f830..d0d3bb50 100644 --- a/search_api.info +++ b/search_api.info @@ -19,6 +19,7 @@ files[] = includes/callback_role_filter.inc files[] = includes/datasource.inc files[] = includes/datasource_entity.inc files[] = includes/datasource_external.inc +files[] = includes/datasource_multiple.inc files[] = includes/exception.inc files[] = includes/index_entity.inc files[] = includes/processor.inc diff --git a/search_api.install b/search_api.install index 850b8280..3650a033 100644 --- a/search_api.install +++ b/search_api.install @@ -191,6 +191,35 @@ function search_api_schema() { 'primary key' => array('item_id', 'index_id'), ); + $schema['search_api_item_string_id'] = array( + 'description' => 'Stores the items which should be indexed for each index, and their status. Used only for items with string IDs.', + 'fields' => array( + 'item_id' => array( + 'description' => "The item's ID.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'index_id' => array( + 'description' => 'The {search_api_index}.id this item belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'changed' => array( + 'description' => 'Either a flag or a timestamp to indicate if or when the item was changed since it was last indexed.', + 'type' => 'int', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'indexes' => array( + 'indexing' => array('index_id', 'changed'), + ), + 'primary key' => array('item_id', 'index_id'), + ); + $schema['search_api_task'] = array( 'description' => 'Stores pending tasks for servers.', 'fields' => array( @@ -1001,3 +1030,38 @@ function search_api_update_7117() { ->execute(); } } + +/** + * Adds the {search_api_item_string_id} table for items with string IDs. + */ +function search_api_update_7118() { + $table = array( + 'description' => 'Stores the items which should be indexed for each index, and their status. Used only for items with string IDs.', + 'fields' => array( + 'item_id' => array( + 'description' => "The item's ID.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'index_id' => array( + 'description' => 'The {search_api_index}.id this item belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'changed' => array( + 'description' => 'Either a flag or a timestamp to indicate if or when the item was changed since it was last indexed.', + 'type' => 'int', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'indexes' => array( + 'indexing' => array('index_id', 'changed'), + ), + 'primary key' => array('item_id', 'index_id'), + ); + db_create_table('search_api_item_string_id', $table); +} diff --git a/search_api.module b/search_api.module index 86b09189..dcec4907 100644 --- a/search_api.module +++ b/search_api.module @@ -647,8 +647,9 @@ function search_api_search_api_index_insert(SearchApiIndex $index) { * Implements hook_search_api_index_update(). */ function search_api_search_api_index_update(SearchApiIndex $index) { - // Call the datasource update function with the table this module provides. + // Call the datasource update function with the tables this module provides. search_api_index_update_datasource($index, 'search_api_item'); + search_api_index_update_datasource($index, 'search_api_item_string_id'); // If the server was changed, we have to call the appropriate service class // hook methods. @@ -857,6 +858,8 @@ function search_api_entity_insert($entity, $type) { list($id) = entity_extract_ids($type, $entity); if (isset($id)) { search_api_track_item_insert($type, array($id)); + $combined_id = $type . '/' . $id; + search_api_track_item_insert('multiple', array($combined_id)); } } @@ -889,6 +892,8 @@ function search_api_entity_update($entity, $type) { if (isset($id)) { search_api_track_item_change($type, array($id)); + $combined_id = $type . '/' . $id; + search_api_track_item_change('multiple', array($combined_id)); } } @@ -910,6 +915,8 @@ function search_api_entity_delete($entity, $type) { list($id) = entity_extract_ids($type, $entity); if (isset($id)) { search_api_track_item_delete($type, array($id)); + $combined_id = $type . '/' . $id; + search_api_track_item_delete('multiple', array($combined_id)); } } @@ -967,16 +974,19 @@ function search_api_flush_caches() { function search_api_search_api_item_type_info() { $types = array(); - foreach (entity_get_property_info() as $type => $property_info) { - if ($info = entity_get_info($type)) { - $types[$type] = array( - 'name' => $info['label'], - 'datasource controller' => 'SearchApiEntityDataSourceController', - 'entity_type' => $type, - ); - } + foreach (search_api_entity_type_options_list() as $type => $label) { + $types[$type] = array( + 'name' => $label, + 'datasource controller' => 'SearchApiEntityDataSourceController', + 'entity_type' => $type, + ); } + $types['multiple'] = array( + 'name' => t('Multiple types'), + 'datasource controller' => 'SearchApiCombinedEntityDataSourceController', + ); + return $types; } @@ -2831,6 +2841,66 @@ function search_api_index_options_list() { return $ret; } +/** + * Options list callback for entity types. + * + * Will only include entity types which specify entity property information. + * + * @return string[] + * An array of entity type machine names mapped to their human-readable + * names. + */ +function search_api_entity_type_options_list() { + $types = array(); + foreach (array_keys(entity_get_property_info()) as $type) { + $info = entity_get_info($type); + if ($info) { + $types[$type] = $info['label']; + } + } + return $types; +} + +/** + * Options list callback for entity type bundles. + * + * Will include all bundles for all entity types which specify entity property + * information, in a format combining both entity type and bundle. + * + * @return string[] + * An array of bundle identifiers mapped to their human-readable names. + */ +function search_api_combined_bundle_options_list() { + $types = array(); + foreach (array_keys(entity_get_property_info()) as $type) { + $info = entity_get_info($type); + if (!empty($info['bundles'])) { + foreach ($info['bundles'] as $bundle => $bundle_info) { + $types["$type:$bundle"] = $bundle_info['label']; + } + } + } + return $types; +} + +/** + * Retrieves a human-readable label for a multi-type index item. + * + * Provided as a non-object alternative to + * SearchApiCombinedEntityDataSourceController::getItemLabel() so it can be used + * as a getter callback. + * + * @param object $item + * An item of the "multiple" item type. + * + * @return string|null + * Either a human-readable label for the item, or NULL if none is available. + */ +function search_api_get_multi_type_item_label($item) { + $label = entity_label($item->item_type, $item->{$item->item_type}); + return $label ? $label : NULL; +} + /** * Shutdown function which indexes all queued items, if any. */ From fa3992d913a667a1e408497486e8a856dcd03c96 Mon Sep 17 00:00:00 2001 From: thepanz Date: Thu, 27 Aug 2015 18:50:28 +0200 Subject: [PATCH 160/278] Issue #1197538 by thePanz, k4v, drunken monkey, ayalon, nadavoid, timodwhit, becw, Elvar: Added support for the "Global: Random" sort in Views. --- CHANGELOG.txt | 2 + contrib/search_api_views/README.txt | 16 ++++++ contrib/search_api_views/includes/query.inc | 54 +++++++++++++++++++-- includes/query.inc | 8 ++- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 468bed37..ee7bc0cb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #1197538 by thePanz, k4v, drunken monkey, ayalon, nadavoid, timodwhit, becw, + Elvar: Added support for the "Global: Random" sort in Views. - #2520934 by drunken monkey: Added an item type for indexing several types of entities in one index. - #2533096 by drunken monkey: Fixed uncaught exception when deleting a server. diff --git a/contrib/search_api_views/README.txt b/contrib/search_api_views/README.txt index bae140fe..b36a7b55 100644 --- a/contrib/search_api_views/README.txt +++ b/contrib/search_api_views/README.txt @@ -24,6 +24,22 @@ When these are present, the normal keywords should be ignored and the related items be returned as results instead. Sorting, filtering and range restriction should all work normally. +"Random sort" feature +--------------------- +This module defines the "Random sort" feature (feature key: +"search_api_random_sort") that allows to randomly sort the results returned by a +search. With a server supporting this, you can use the "Global: Random" sort to +sort the view's results randomly. Every time the query is run a different +sorting will be provided. + +For developers: +A service class that wants to support this feature has to check for a +"search_api_random" field in the search query's sorts and insert a random sort +in that position. If the query is sorted in this way, then the +"search_api_random_sort" query option can contain additional options for the +random sort, as an associative array with any of the following keys: +- seed: A numeric seed value to use for the random sort. + "Facets block" display ---------------------- Most features should be clear to users of Views. However, the module also diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index ee551bba..923570bb 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -122,19 +122,63 @@ class SearchApiViewsQuery extends views_plugin_query { } /** - * Add a sort to the query. + * Adds a sort to the query. * - * @param $selector + * @param string $selector * The field to sort on. All indexed fields of the index are valid values. - * In addition, the special fields 'search_api_relevance' (sort by - * relevance) and 'search_api_id' (sort by item id) may be used. - * @param $order + * In addition, these special fields may be used: + * - search_api_relevance: sort by relevance; + * - search_api_id: sort by item id; + * - search_api_random: random sort (available only if the server supports + * the "search_api_random_sort" feature). + * @param string $order * The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'. */ public function add_selector_orderby($selector, $order = 'ASC') { $this->query->sort($selector, $order); } + /** + * Provides a sorting method as present in the Views default query plugin. + * + * This is provided so that the "Global: Random" sort included in Views will + * work properly with Search API Views. Random sorting is only supported if + * the active search server supports the "search_api_random_sort" feature, + * though, otherwise the call will be ignored. + * + * This method can only be used to sort randomly, as would be done with the + * default query plugin. All other calls are ignored. + * + * @param string|null $table + * Only "rand" is recognized here, all other calls are ignored. + * @param string|null $field + * Is ignored and only present for compatibility reasons. + * @param string $order + * Either "ASC" or "DESC". + * @param string|null $alias + * Is ignored and only present for compatibility reasons. + * @param array $params + * The following optional parameters are recognized: + * - seed: a predefined seed for the random generator. + * + * @see views_plugin_query_default::add_orderby() + */ + public function add_orderby($table, $field = NULL, $order = 'ASC', $alias = '', $params = array()) { + $server = $this->getIndex()->server(); + if ($table == 'rand') { + if ($server->supportsFeature('search_api_random_sort')) { + $this->add_selector_orderby('search_api_random', $order); + if ($params) { + $this->setOption('search_api_random_sort', $params); + } + } + else { + $variables['%server'] = $server->label(); + watchdog('search_api_views', 'Tried to sort results randomly on server %server which does not support random sorting.', $variables, WATCHDOG_WARNING); + } + } + } + /** * Defines the options used by this query plugin. * diff --git a/includes/query.inc b/includes/query.inc index b005cf73..1935ff03 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -153,7 +153,9 @@ interface SearchApiQueryInterface { * * @param string $field * The field to sort by. The special fields 'search_api_relevance' (sort by - * relevance) and 'search_api_id' (sort by item id) may be used. + * relevance) and 'search_api_id' (sort by item id) may be used. Also, if + * the search server supports the "search_api_random_sort" feature, the + * "search_api_random_sort" special field can be used to sort randomly. * @param string $order * The order to sort items in - either 'ASC' or 'DESC'. * @@ -586,6 +588,10 @@ class SearchApiQuery implements SearchApiQueryInterface { 'search_api_relevance' => array('type' => 'decimal'), 'search_api_id' => array('type' => 'integer'), ); + if ($this->getIndex()->server()->supportsFeature('search_api_random_sort')) { + $fields['search_api_random'] = array('type' => 'integer'); + } + if (empty($fields[$field])) { throw new SearchApiException(t('Trying to sort on unknown field @field.', array('@field' => $field))); } From 03ea0c05ca6afc33b73b4b01e8649977d2c8c467 Mon Sep 17 00:00:00 2001 From: ptmkenny Date: Sat, 29 Aug 2015 09:27:17 +0200 Subject: [PATCH 161/278] Issue #2491175 by ptmkenny, drunken monkey: Added a data alteration for filtering out blocked users. --- CHANGELOG.txt | 2 ++ includes/callback_user_status.inc | 31 +++++++++++++++++++++++++++++++ search_api.info | 1 + search_api.module | 5 +++++ 4 files changed, 39 insertions(+) create mode 100644 includes/callback_user_status.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ee7bc0cb..1e477269 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2491175 by ptmkenny, drunken monkey: Added a data alteration for filtering + out blocked users. - #1197538 by thePanz, k4v, drunken monkey, ayalon, nadavoid, timodwhit, becw, Elvar: Added support for the "Global: Random" sort in Views. - #2520934 by drunken monkey: Added an item type for indexing several types of diff --git a/includes/callback_user_status.inc b/includes/callback_user_status.inc new file mode 100644 index 00000000..541f1b2d --- /dev/null +++ b/includes/callback_user_status.inc @@ -0,0 +1,31 @@ +getEntityType() == 'user'; + } + + /** + * {@inheritdoc} + */ + public function alterItems(array &$items) { + foreach ($items as $id => $account) { + if (empty($account->status)) { + unset($items[$id]); + } + } + } + +} diff --git a/search_api.info b/search_api.info index d0d3bb50..2c4ba14c 100644 --- a/search_api.info +++ b/search_api.info @@ -16,6 +16,7 @@ files[] = includes/callback_language_control.inc files[] = includes/callback_node_access.inc files[] = includes/callback_node_status.inc files[] = includes/callback_role_filter.inc +files[] = includes/callback_user_status.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 dcec4907..f87ced8d 100644 --- a/search_api.module +++ b/search_api.module @@ -1068,6 +1068,11 @@ function search_api_search_api_alter_callback_info() { 'description' => t('Exclude unpublished nodes from the index. Caution: This only affects the indexed nodes themselves. If an enabled node has references to disabled nodes, those will still be indexed (or displayed) normally.'), 'class' => 'SearchApiAlterNodeStatus', ); + $callbacks['search_api_alter_user_status'] = array( + 'name' => t('Exclude blocked users'), + 'description' => t('Exclude blocked users from the index. Caution: This only affects the indexed users themselves. If an active user account includes a reference to a disabled user, that reference will still be indexed (or displayed) normally.'), + 'class' => 'SearchApiAlterUserStatus', + ); return $callbacks; } From 8c217b1755618444d119898afb9ae080701604b8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 29 Aug 2015 19:02:53 +0200 Subject: [PATCH 162/278] Issue #2502819: Fixed example code for hook_search_api_query_alter(). --- CHANGELOG.txt | 1 + search_api.api.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1e477269..8d557539 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2502819: Fixed example code for hook_search_api_query_alter(). - #2491175 by ptmkenny, drunken monkey: Added a data alteration for filtering out blocked users. - #1197538 by thePanz, k4v, drunken monkey, ayalon, nadavoid, timodwhit, becw, diff --git a/search_api.api.php b/search_api.api.php index f9821081..e5c2ca62 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -341,7 +341,7 @@ function hook_search_api_query_alter(SearchApiQueryInterface $query) { // Exclude entities with ID 0. (Assume the ID field is always indexed.) if ($query->getIndex()->getEntityType()) { $info = entity_get_info($query->getIndex()->getEntityType()); - $query->condition($info['entity keys']['id'], 0, '!='); + $query->condition($info['entity keys']['id'], 0, '<>'); } } From b85d9b9b4277769bce11ddb20e16acf449980246 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 30 Aug 2015 21:08:51 +0200 Subject: [PATCH 163/278] Adapted CHANGELOG.txt to 1.16 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8d557539..7f4a8bff 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.16 (2015-08-30): +----------------------------- - #2502819: Fixed example code for hook_search_api_query_alter(). - #2491175 by ptmkenny, drunken monkey: Added a data alteration for filtering out blocked users. From e5a801242ee16d07d88e54fd3204516c2fd0ff5e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 30 Aug 2015 21:08:51 +0200 Subject: [PATCH 164/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7f4a8bff..bdef3930 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xx/xx/xxxx): +--------------------------------- + Search API 1.16 (2015-08-30): ----------------------------- - #2502819: Fixed example code for hook_search_api_query_alter(). From 1f03fe1aa313e5b9179824ee04ba0b622a02d932 Mon Sep 17 00:00:00 2001 From: alasdair86 Date: Sun, 6 Sep 2015 12:01:53 +0200 Subject: [PATCH 165/278] Issue #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds. --- CHANGELOG.txt | 1 + search_api.module | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bdef3930..fef45dc8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds. Search API 1.16 (2015-08-30): ----------------------------- diff --git a/search_api.module b/search_api.module index f87ced8d..b099741d 100644 --- a/search_api.module +++ b/search_api.module @@ -883,7 +883,7 @@ function search_api_entity_update($entity, $type) { list($id, , $new_bundle) = entity_extract_ids($type, $entity); // Check if the entity's bundle changed. - if (isset($entity->original)) { + if (!empty($entity->original)) { list(, , $old_bundle) = entity_extract_ids($type, $entity->original); if ($new_bundle != $old_bundle) { _search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle); From 11dcadeda547c852dcad445f3e83c30e5a0fe29e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 23 Sep 2015 14:02:07 +0200 Subject: [PATCH 166/278] Issue #2524314 by drunken monkey: Fixed bundle-setting for taxonomy term indexes. --- CHANGELOG.txt | 1 + includes/datasource_entity.inc | 58 +++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fef45dc8..b583a861 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2524314 by drunken monkey: Fixed bundle-setting for taxonomy term indexes. - #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds. Search API 1.16 (2015-08-30): diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 59f5bd93..f3470867 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -160,7 +160,17 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id)); $query->addExpression('1', 'changed'); if ($bundles = $this->getIndexBundles($index)) { - $query->condition($this->bundleKey, $bundles); + $bundle_column = $this->bundleKey; + if (!db_field_exists($table, $bundle_column)) { + if ($this->entityType == 'taxonomy_term') { + $bundle_column = 'vid'; + $bundles = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name IN (:bundles)', array(':bundles' => $bundles))->fetchCol(); + } + else { + $this->startTrackingFallback(array($index->machine_name => $index)); + } + } + $query->condition($bundle_column, $bundles); } // INSERT ... SELECT ... @@ -170,20 +180,38 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } } else { - // In the absence of a 'base table', use the slower way of retrieving the - // items and inserting them "manually". For each index we get the item IDs - // (since selected bundles might differ) and insert all of them as new. - foreach ($indexes as $index) { - $query = new EntityFieldQuery(); - $query->entityCondition('entity_type', $this->entityType); - if ($bundles = $this->getIndexBundles($index)) { - $query->entityCondition('bundle', $bundles); - } - $result = $query->execute(); - $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array(); - if ($ids) { - $this->trackItemInsert($ids, array($index)); - } + $this->startTrackingFallback($indexes); + } + } + + /** + * Initializes tracking of the index status of items for the given indexes. + * + * Fallback for when the items cannot directly be loaded into + * {search_api_item} via "INSERT INTO … SELECT …". + * + * @param SearchApiIndex[] $indexes + * The indexes for which item tracking should be initialized. + * + * @throws SearchApiDataSourceException + * Thrown if any error state was encountered. + * + * @see SearchApiEntityDataSourceController::startTracking() + */ + protected function startTrackingFallback(array $indexes) { + // In the absence of a 'base table', use the slower way of retrieving the + // items and inserting them "manually". For each index we get the item IDs + // (since selected bundles might differ) and insert all of them as new. + foreach ($indexes as $index) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', $this->entityType); + if ($bundles = $this->getIndexBundles($index)) { + $query->entityCondition('bundle', $bundles); + } + $result = $query->execute(); + $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array(); + if ($ids) { + $this->trackItemInsert($ids, array($index)); } } } From cd30e7563c8c3ee3315bdb3dde168987dbd1b406 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 6 Oct 2015 09:14:07 +0200 Subject: [PATCH 167/278] Issue #2565005 by drunken monkey: Properly escape labels of "checkboxes"/"radios" options --- CHANGELOG.txt | 2 ++ includes/callback_language_control.inc | 2 +- includes/datasource_entity.inc | 2 +- includes/datasource_multiple.inc | 2 +- includes/processor.inc | 2 +- includes/processor_highlight.inc | 4 ++-- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b583a861..fbc995e8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2565005 by drunken monkey: Properly escape labels of "checkboxes"/"radios" + options - #2524314 by drunken monkey: Fixed bundle-setting for taxonomy term indexes. - #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds. diff --git a/includes/callback_language_control.inc b/includes/callback_language_control.inc index 233852c1..d406d35d 100644 --- a/includes/callback_language_control.inc +++ b/includes/callback_language_control.inc @@ -70,7 +70,7 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback { foreach ($list as $lang) { $name = t($lang->name); $native = $lang->native; - $languages[$lang->language] = ($name == $native) ? $name : "$name ($native)"; + $languages[$lang->language] = check_plain(($name == $native) ? $name : "$name ($native)"); if (!$lang->enabled) { $languages[$lang->language] .= ' [' . t('disabled') . ']'; } diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index f3470867..f977453a 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -253,7 +253,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon '#type' => 'checkboxes', '#title' => t('Bundles'), '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'), - '#options' => $options, + '#options' => array_map('check_plain', $options), '#attributes' => array('class' => array('search-api-checkboxes-list')), '#disabled' => !empty($form_state['index']), ); diff --git a/includes/datasource_multiple.inc b/includes/datasource_multiple.inc index 75adf459..7e5d4de2 100644 --- a/includes/datasource_multiple.inc +++ b/includes/datasource_multiple.inc @@ -252,7 +252,7 @@ class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataS '#type' => 'checkboxes', '#title' => t('Entity types'), '#description' => t('Select the entity types which should be included in this index.'), - '#options' => search_api_entity_type_options_list(), + '#options' => array_map('check_plain', search_api_entity_type_options_list()), '#attributes' => array('class' => array('search-api-checkboxes-list')), '#disabled' => !empty($form_state['index']), '#required' => TRUE, diff --git a/includes/processor.inc b/includes/processor.inc index 08fb02cf..fe92882c 100644 --- a/includes/processor.inc +++ b/includes/processor.inc @@ -172,7 +172,7 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface $default_fields = drupal_map_assoc(array_keys($this->options['fields'])); } foreach ($fields as $name => $field) { - $field_options[$name] = $field['name']; + $field_options[$name] = check_plain($field['name']); if (!empty($default_fields[$name]) || (!isset($this->options['fields']) && $this->testField($name, $field))) { $default_fields[$name] = $name; } diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index f094d9d4..566599af 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -86,12 +86,12 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { ), ), ); - // Exclude certain fulltextfields + // Exclude certain fulltext fields. $fields = $this->index->getFields(); $fulltext_fields = array(); foreach ($this->index->getFulltextFields() as $field) { if (isset($fields[$field])) { - $fulltext_fields[$field] = $fields[$field]['name'] . ' (' . $field . ')'; + $fulltext_fields[$field] = check_plain($fields[$field]['name'] . ' (' . $field . ')'); } } $form['exclude_fields'] = array( From 8f30ddbd1027b3ce056e84a171389734a73fb5c7 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 15 Oct 2015 19:43:26 +0200 Subject: [PATCH 168/278] Fixed the docs for SearchApiQueryInterface::execute(). --- includes/query.inc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/query.inc b/includes/query.inc index 1935ff03..3c244eb2 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -220,6 +220,9 @@ interface SearchApiQueryInterface { * - postprocessing: Preparing the results for returning. * Additional metadata may be returned in other keys. Only 'result count' * and 'result' always have to be set, all other entries are optional. + * + * @throws SearchApiException + * If an error prevented the search from completing. */ public function execute(); From 7c3045576889269bc3c98142e59b9a45ccb2324e Mon Sep 17 00:00:00 2001 From: "joseph.olstad" Date: Thu, 22 Oct 2015 18:09:57 +0200 Subject: [PATCH 169/278] Issue #2567775 by joseph.olstad, drunken monkey: Fixed handling of broken HTML in the "HTML filter" processor. --- CHANGELOG.txt | 2 ++ includes/processor_html_filter.inc | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fbc995e8..28b931e6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2567775 by joseph.olstad, drunken monkey: Fixed handling of broken HTML in + the "HTML filter" processor. - #2565005 by drunken monkey: Properly escape labels of "checkboxes"/"radios" options - #2524314 by drunken monkey: Fixed bundle-setting for taxonomy term indexes. diff --git a/includes/processor_html_filter.inc b/includes/processor_html_filter.inc index 0e714c32..180c9c18 100644 --- a/includes/processor_html_filter.inc +++ b/includes/processor_html_filter.inc @@ -120,7 +120,9 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { ); } $text = substr($text, $pos + 1); - preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m); + if (!preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m)) { + continue; + } $text = substr($text, strpos($text, '>') + 1); if ($m[1]) { // Closing tag. From a5d4bf7a6ea06dbfb4713244532e704a7c9ef701 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 3 Nov 2015 18:52:29 +0100 Subject: [PATCH 170/278] Issue #2563793 by drunken monkey, smitty, ReBa: Fixed Views base table definition for "Multiple types" indexes. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/search_api_views.views.inc | 9 ++------- includes/index_entity.inc | 7 ++++++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 28b931e6..8f04f2c8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2563793 by drunken monkey, smitty, ReBa: Fixed Views base table definition + for "Multiple types" indexes. - #2567775 by joseph.olstad, drunken monkey: Fixed handling of broken HTML in the "HTML filter" processor. - #2565005 by drunken monkey: Properly escape labels of "checkboxes"/"radios" diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index a87e5bcc..ff52d692 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -11,7 +11,6 @@ function search_api_views_views_data() { try { $data = array(); - $entity_types = entity_get_info(); foreach (search_api_index_load_multiple(FALSE) as $index) { // Fill in base data. $key = 'search_api_index_' . $index->machine_name; @@ -25,12 +24,8 @@ function search_api_views_views_data() { 'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)), 'query class' => 'search_api_views_query', ); - if (isset($entity_types[$index->getEntityType()])) { - $table['table'] += array( - 'entity type' => $index->getEntityType(), - 'skip entity load' => TRUE, - ); - } + $table['table']['entity type'] = $index->getEntityType(); + $table['table']['skip entity load'] = TRUE; try { $wrapper = $index->entityWrapper(NULL, FALSE); diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 824bf129..7a4234a5 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -934,7 +934,12 @@ class SearchApiIndex extends Entity { $i = $only_indexed ? 1 : 0; if (!isset($this->fulltext_fields[$i])) { $this->fulltext_fields[$i] = array(); - $fields = $only_indexed ? $this->options['fields'] : $this->getFields(FALSE); + if ($only_indexed) { + $fields = isset($this->options['fields']) ? $this->options['fields'] : array(); + } + else { + $fields = $this->getFields(FALSE); + } foreach ($fields as $key => $field) { if (search_api_is_text_type($field['type'])) { $this->fulltext_fields[$i][] = $key; From 2df931cdef9c60ebc5f5a47c17bf97cb2e66c03c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 3 Nov 2015 18:56:48 +0100 Subject: [PATCH 171/278] Issue #2565743 by drunken monkey: Fixed creation of comment indexes with specific bundles. --- CHANGELOG.txt | 2 ++ includes/datasource_entity.inc | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8f04f2c8..b1ff8804 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xx/xx/xxxx): --------------------------------- +- #2565743 by drunken monkey: Fixed creation of comment indexes with specific + bundles. - #2563793 by drunken monkey, smitty, ReBa: Fixed Views base table definition for "Multiple types" indexes. - #2567775 by joseph.olstad, drunken monkey: Fixed handling of broken HTML in diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index f977453a..034bb525 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -166,6 +166,25 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon $bundle_column = 'vid'; $bundles = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name IN (:bundles)', array(':bundles' => $bundles))->fetchCol(); } + elseif ($this->entityType == 'comment') { + // Comments are significantly more complicated, since they don't + // store their bundle explicitly in their database table. Instead, + // we need to get all the nodes from the enabled types and filter + // by those. + $bundle_column = 'nid'; + $node_types = array(); + foreach ($bundles as $bundle) { + if (substr($bundle, 0, 13) === 'comment_node_') { + $node_types[] = substr($bundle, 13); + } + } + if ($node_types) { + $bundles = db_query('SELECT nid FROM {node} WHERE type IN (:bundles)', array(':bundles' => $node_types))->fetchCol(); + } + else { + return; + } + } else { $this->startTrackingFallback(array($index->machine_name => $index)); } From 20f3662e149326a42e5d996113497ffed51d2b69 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 9 Nov 2015 13:38:35 +0100 Subject: [PATCH 172/278] Fixed the CHANGELOG.txt format. --- CHANGELOG.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b1ff8804..8a6b4a57 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,4 @@ -Search API 1.x, dev (xx/xx/xxxx): +Search API 1.x, dev (xxxx-xx-xx): --------------------------------- - #2565743 by drunken monkey: Fixed creation of comment indexes with specific bundles. From 0edea1dc90776f3b6da337706fabdd620dcb6b44 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 10 Nov 2015 09:35:56 +0100 Subject: [PATCH 173/278] Issue #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for empty $item_ids. --- includes/datasource.inc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/datasource.inc b/includes/datasource.inc index 0c72f014..eec9a79c 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -22,7 +22,7 @@ interface SearchApiDataSourceControllerInterface { /** - * Constructs a new data source controller. + * Constructs an SearchApiDataSourceControllerInterface object. * * @param string $type * The item type for which this controller is created. @@ -47,7 +47,7 @@ interface SearchApiDataSourceControllerInterface { * Loads items of the type of this data source controller. * * @param array $ids - * The IDs of the items to laod. + * The IDs of the items to load. * * @return array * The loaded items, keyed by ID. @@ -592,7 +592,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * {@inheritdoc} */ public function trackItemInsert(array $item_ids, array $indexes) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return; } @@ -622,7 +622,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * {@inheritdoc} */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return NULL; } @@ -652,7 +652,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou */ public function trackItemQueued($item_ids, SearchApiIndex $index) { $this->checkIndex($index); - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return; } $update = db_update($this->table) @@ -670,7 +670,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * {@inheritdoc} */ public function trackItemIndexed(array $item_ids, SearchApiIndex $index) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return; } $this->checkIndex($index); @@ -687,7 +687,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou * {@inheritdoc} */ public function trackItemDelete(array $item_ids, array $indexes) { - if (!$this->table) { + if (!$this->table || $item_ids === array()) { return NULL; } From ed93634214b933e6031e376b58669be97e219527 Mon Sep 17 00:00:00 2001 From: thepanz Date: Tue, 17 Nov 2015 22:10:47 +0100 Subject: [PATCH 174/278] Issue #2570879 by thePanz, drunken monkey: Added sorting of remembered search IDs. --- CHANGELOG.txt | 1 + .../plugins/facetapi/adapter.inc | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8a6b4a57..89f29d20 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2570879 by thePanz, drunken monkey: Added sorting of remembered search IDs. - #2565743 by drunken monkey: Fixed creation of comment indexes with specific bundles. - #2563793 by drunken monkey, smitty, ReBa: Fixed Views base table definition diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index cddcf56d..c9855b71 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -78,13 +78,10 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { // displayed. $facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(); + // Remember this search ID, if necessary. + $this->rememberSearchId($index_id, $search_id); + if (array_search($search_id, $facet_search_ids) === FALSE) { - $search_ids = variable_get('search_api_facets_search_ids', array()); - if (empty($search_ids[$index_id][$search_id])) { - // Remember this search ID. - $search_ids[$index_id][$search_id] = $search_id; - variable_set('search_api_facets_search_ids', $search_ids); - } if (!$default_true) { continue; // We are only to show facets for explicitly named search ids. } @@ -103,6 +100,23 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { } } + /** + * Adds a search ID to the list of known searches for an index. + * + * @param string $index_id + * The machine name of the search index. + * @param string $search_id + * The identifier of the executed search. + */ + protected function rememberSearchId($index_id, $search_id) { + $search_ids = variable_get('search_api_facets_search_ids', array()); + if (empty($search_ids[$index_id][$search_id])) { + $search_ids[$index_id][$search_id] = $search_id; + asort($search_ids[$index_id]); + variable_set('search_api_facets_search_ids', $search_ids); + } + } + /** * Add the given facet to the query. */ From 4c40fae2589d02fbe8c7abb4e026e177ec1c17da Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 28 Nov 2015 14:16:26 +0100 Subject: [PATCH 175/278] Issue #2592231 by drunken monkey, balintcsaba: Fixed ignored item language when viewing translated items. --- CHANGELOG.txt | 2 ++ includes/callback_add_viewed_entity.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 89f29d20..b6c1acb8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2592231 by drunken monkey, balintcsaba: Fixed ignored item language when + viewing translated items. - #2570879 by thePanz, drunken monkey: Added sorting of remembered search IDs. - #2565743 by drunken monkey: Fixed creation of comment indexes with specific bundles. diff --git a/includes/callback_add_viewed_entity.inc b/includes/callback_add_viewed_entity.inc index 06b05c38..bb2ae07d 100644 --- a/includes/callback_add_viewed_entity.inc +++ b/includes/callback_add_viewed_entity.inc @@ -74,7 +74,7 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback { // we use try/catch. This will at least prevent some errors, even though // it's no protection against fatal errors and the like. try { - $render = entity_view($type, array(entity_id($type, $item) => $item), $mode); + $render = entity_view($type, array(entity_id($type, $item) => $item), $mode, $item->search_api_language); $text = render($render); if (!$text) { $item->search_api_viewed = NULL; From 60008e60f0013803c27e7767caaccd90cd1a4797 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 7 Dec 2015 12:54:25 +0100 Subject: [PATCH 176/278] Issue #2583263 by drunken monkey: Fixed Views integration in combination with Search API ET and similar modules. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b6c1acb8..93773824 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2583263 by drunken monkey: Fixed Views integration in combination with + Search API ET and similar modules. - #2592231 by drunken monkey, balintcsaba: Fixed ignored item language when viewing translated items. - #2570879 by thePanz, drunken monkey: Added sorting of remembered search IDs. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 923570bb..958ee7a3 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -538,7 +538,7 @@ class SearchApiViewsQuery extends views_plugin_query { * query backend. */ public function get_result_wrappers($results, $relationship = NULL, $field = NULL) { - $type = $this->index->item_type; + $type = $this->index->getEntityType() ? $this->index->getEntityType() : $this->index->item_type; $wrappers = array(); $load_items = array(); foreach ($results as $row_index => $row) { From 18f5d1a8ee98d4ac46cc585b3f0f91ebb518d05d Mon Sep 17 00:00:00 2001 From: kingmackenzie Date: Mon, 7 Dec 2015 13:18:53 +0100 Subject: [PATCH 177/278] Issue #2529262 by kingmackenzie, stefan.r: Added an option to Views date filters to choose the format used by date popup. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_date.inc | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 93773824..023e0488 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2529262 by kingmackenzie, stefan.r: Added an option to Views date filters to + choose the format used by date popup. - #2583263 by drunken monkey: Fixed Views integration in combination with Search API ET and similar modules. - #2592231 by drunken monkey, balintcsaba: Fixed ignored item language when diff --git a/contrib/search_api_views/includes/handler_filter_date.inc b/contrib/search_api_views/includes/handler_filter_date.inc index 4b856dd8..319517c1 100644 --- a/contrib/search_api_views/includes/handler_filter_date.inc +++ b/contrib/search_api_views/includes/handler_filter_date.inc @@ -16,6 +16,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { public function option_definition() { return parent::option_definition() + array( 'widget_type' => array('default' => 'default'), + 'date_popup_format' => array('default' => 'm/d/Y'), 'year_range' => array('default' => '-3:+3'), ); } @@ -43,6 +44,19 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { '#default_value' => $this->options['widget_type'], '#options' => $widget_options, ); + $form['date_popup_format'] = array( + '#type' => 'textfield', + '#title' => t('Date format'), + '#default_value' => $this->options['date_popup_format'], + '#description' => t('A date in any format understood by PHP. For example, "Y-m-d" or "m/d/Y".', array( + '@doc-link' => 'http://php.net/manual/en/function.date.php' + )), + '#states' => array( + 'visible' => array( + ':input[name="options[widget_type]"]' => array('value' => 'date_popup'), + ), + ), + ); } if (module_exists('date_api')) { @@ -74,7 +88,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { // according to what date_popup expects. if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) { $form['value']['#type'] = 'date_popup'; - $form['value']['#date_format'] = 'm/d/Y'; + $form['value']['#date_format'] = $this->options['date_popup_format']; $form['value']['#date_year_range'] = $this->options['year_range']; unset($form['value']['#description']); } From 2bc929e1a7c49df5e284329af4e3645fb56174f5 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 7 Dec 2015 13:20:17 +0100 Subject: [PATCH 178/278] Small code formatting change. --- contrib/search_api_views/includes/handler_filter_date.inc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/search_api_views/includes/handler_filter_date.inc b/contrib/search_api_views/includes/handler_filter_date.inc index 319517c1..c7897245 100644 --- a/contrib/search_api_views/includes/handler_filter_date.inc +++ b/contrib/search_api_views/includes/handler_filter_date.inc @@ -36,8 +36,12 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { */ public function extra_options_form(&$form, &$form_state) { parent::extra_options_form($form, $form_state); + if (module_exists('date_popup')) { - $widget_options = array('default' => 'Default', 'date_popup' => 'Date popup'); + $widget_options = array( + 'default' => 'Default', + 'date_popup' => 'Date popup', + ); $form['widget_type'] = array( '#type' => 'radios', '#title' => t('Date selection form element'), From d0cedabc73381021b9479b805629ced6783bba9d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 8 Dec 2015 16:32:35 +0100 Subject: [PATCH 179/278] Issue #2603500 by drunken monkey, krishna savithraj: Fixed Views fulltext searches for keyword "All". --- CHANGELOG.txt | 2 + .../includes/handler_filter_text.inc | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 023e0488..56761752 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2603500 by drunken monkey, krishna savithraj: Fixed Views fulltext searches + for keyword "All". - #2529262 by kingmackenzie, stefan.r: Added an option to Views date filters to choose the format used by date popup. - #2583263 by drunken monkey: Fixed Views integration in combination with diff --git a/contrib/search_api_views/includes/handler_filter_text.inc b/contrib/search_api_views/includes/handler_filter_text.inc index fae248a6..83152a1d 100644 --- a/contrib/search_api_views/includes/handler_filter_text.inc +++ b/contrib/search_api_views/includes/handler_filter_text.inc @@ -17,4 +17,52 @@ class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter { return array('=' => t('contains'), '<>' => t("doesn't contain")); } + /** + * Determines whether input from the exposed filters affects this filter. + * + * Overridden to not treat "All" differently. + * + * @param array $input + * The user input from the exposed filters. + * + * @return bool + * TRUE if the input should change the behavior of this filter. + */ + public function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) { + $this->operator = $input[$this->options['expose']['operator_id']]; + } + + if (!empty($this->options['expose']['identifier'])) { + $value = $input[$this->options['expose']['identifier']]; + + // Various ways to check for the absence of non-required input. + if (empty($this->options['expose']['required'])) { + if (($this->operator == 'empty' || $this->operator == 'not empty') && $value === '') { + $value = ' '; + } + + if (!empty($this->always_multiple) && $value === '') { + return FALSE; + } + } + + if (isset($value)) { + $this->value = $value; + if (empty($this->always_multiple) && empty($this->options['expose']['multiple'])) { + $this->value = array($value); + } + } + else { + return FALSE; + } + } + + return TRUE; + } + } From ae85052d912654b4feeb22047f84fa7a315669e1 Mon Sep 17 00:00:00 2001 From: Hubbs Date: Wed, 9 Dec 2015 18:49:21 +0100 Subject: [PATCH 180/278] Issue #2611726 by Hubbs, rakesh.gectcr: Fixed several typos. --- CHANGELOG.txt | 1 + includes/exception.inc | 2 +- includes/query.inc | 4 ++-- includes/server_entity.inc | 8 ++++---- search_api.module | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 56761752..f67b0bfc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2611726 by Hubbs, rakesh.gectcr: Fixed several typos. - #2603500 by drunken monkey, krishna savithraj: Fixed Views fulltext searches for keyword "All". - #2529262 by kingmackenzie, stefan.r: Added an option to Views date filters to diff --git a/includes/exception.inc b/includes/exception.inc index f69ddd59..2e4f7903 100644 --- a/includes/exception.inc +++ b/includes/exception.inc @@ -19,7 +19,7 @@ class SearchApiException extends Exception { */ public function __construct($message = NULL) { if (!$message) { - $message = t('An error occcurred in the Search API framework.'); + $message = t('An error occurred in the Search API framework.'); } parent::__construct($message); } diff --git a/includes/query.inc b/includes/query.inc index 3c244eb2..e781641f 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -471,8 +471,8 @@ class SearchApiQuery implements SearchApiQueryInterface { ); $modes['terms'] = array( 'name' => t('Multiple terms'), - 'description' => t('The query is interpreted as multiple keywords seperated by spaces. ' . - 'Keywords containing spaces may be "quoted". Quoted keywords must still be seperated by spaces.'), + 'description' => t('The query is interpreted as multiple keywords separated by spaces. ' . + 'Keywords containing spaces may be "quoted". Quoted keywords must still be separated by spaces.'), ); // @todo Add fourth mode for complicated expressions, e.g.: »"vanilla ice" OR (love NOT hate)« return $modes; diff --git a/includes/server_entity.inc b/includes/server_entity.inc index 4a991166..6ab8bd79 100644 --- a/includes/server_entity.inc +++ b/includes/server_entity.inc @@ -241,7 +241,7 @@ class SearchApiServer extends Entity { /** * Adds a new index to this server. * - * If an exception in the service class implementation of this method occcurs, + * If an exception in the service class implementation of this method occurs, * it will be caught and the operation saved as an pending server task. * * @see SearchApiServiceInterface::addIndex() @@ -268,7 +268,7 @@ class SearchApiServer extends Entity { * If the service class implementation of the method returns TRUE, this will * automatically take care of marking the items on the index for re-indexing. * - * If an exception in the service class implementation of this method occcurs, + * If an exception in the service class implementation of this method occurs, * it will be caught and the operation saved as an pending server task. * * @see SearchApiServiceInterface::fieldsUpdated() @@ -296,7 +296,7 @@ class SearchApiServer extends Entity { /** * Removes an index from this server. * - * If an exception in the service class implementation of this method occcurs, + * If an exception in the service class implementation of this method occurs, * it will be caught and the operation saved as an pending server task. * * @see SearchApiServiceInterface::removeIndex() @@ -334,7 +334,7 @@ class SearchApiServer extends Entity { /** * Deletes indexed items from this server. * - * If an exception in the service class implementation of this method occcurs, + * If an exception in the service class implementation of this method occurs, * it will be caught and the operation saved as an pending server task. * * @see SearchApiServiceInterface::deleteItems() diff --git a/search_api.module b/search_api.module index b099741d..91a0d00e 100644 --- a/search_api.module +++ b/search_api.module @@ -2,7 +2,7 @@ /** * @file - * Provides a flexible framework for implementing search servives. + * Provides a flexible framework for implementing search services. */ /** @@ -2196,7 +2196,7 @@ function search_api_extract_inner_type($type) { * * Modules implementing other datasource controllers, that use a table other * than {search_api_item}, can use this function, too. It should be called - * uncoditionally in a hook_search_api_index_update() implementation. If this + * unconditionally in a hook_search_api_index_update() implementation. If this * function isn't used, similar code should be added there. * * However, note that this is only necessary (and this function should only be @@ -3174,7 +3174,7 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, * @param boolean $success * Whether the batch finished successfully. * @param array $results - * Detailed informations about the result. + * Detailed information about the result. */ function _search_api_batch_indexing_finished($success, $results) { // Check if called from drush. From 2adee7bcf15e4977ae465af4a4579a8074748b92 Mon Sep 17 00:00:00 2001 From: "artem.kolotilkin" Date: Wed, 9 Dec 2015 19:26:52 +0100 Subject: [PATCH 181/278] Issue #2613054 by temkin: Fixed the "search-api-index" Drush command to allow setting further options when indexing on all indexes. --- CHANGELOG.txt | 2 ++ search_api.drush.inc | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f67b0bfc..41db2c62 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2613054 by temkin: Fixed the "search-api-index" Drush command to allow + setting further options when indexing on all indexes. - #2611726 by Hubbs, rakesh.gectcr: Fixed several typos. - #2603500 by drunken monkey, krishna savithraj: Fixed Views fulltext searches for keyword "All". diff --git a/search_api.drush.inc b/search_api.drush.inc index 9edefe94..61957b9a 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -71,9 +71,10 @@ function search_api_drush_command() { 'drush sapi-i default_node_index' => dt('Index items for the index with the machine name !name.', array('!name' => 'default_node_index')), 'drush sapi-i 1 100' => dt("Index a maximum number of !limit items (index's cron batch size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!id' => 1)), 'drush sapi-i 1 100 10' => dt("Index a maximum number of !limit items (!batch_size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!batch_size' => 10, '!id' => 1)), + 'drush sapi-i 0 0 100' => dt("Index all items of all indexes with !batch_size items per batch run.", array('!batch_size' => 100)), ), 'arguments' => array( - 'index_id' => dt('The numeric ID or machine name of an index.'), + 'index_id' => dt('The numeric ID or machine name of an index. Set to 0 to index all indexes. Defaults to 0 (index all).'), 'limit' => dt("The number of items to index (index's cron batch size items per run). Set to 0 to index all items. Defaults to 0 (index all)."), 'batch_size' => dt("The number of items to index per batch run. Set to 0 to index all items at once. Defaults to the index's cron batch size."), ), @@ -326,6 +327,7 @@ function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = N if (search_api_drush_static(__FUNCTION__)) { return; } + $index_id = !empty($index_id) ? $index_id : NULL; $indexes = search_api_drush_get_index($index_id); if (empty($indexes)) { return; From 3547208a438d420422071cde71e4b30671eac8e2 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 10 Dec 2015 12:26:37 +0100 Subject: [PATCH 182/278] Fixed some minor typos in doc comments. --- includes/query.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/query.inc b/includes/query.inc index e781641f..30cd6e9a 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -155,7 +155,7 @@ interface SearchApiQueryInterface { * The field to sort by. The special fields 'search_api_relevance' (sort by * relevance) and 'search_api_id' (sort by item id) may be used. Also, if * the search server supports the "search_api_random_sort" feature, the - * "search_api_random_sort" special field can be used to sort randomly. + * "search_api_random" special field can be used to sort randomly. * @param string $order * The order to sort items in - either 'ASC' or 'DESC'. * @@ -219,7 +219,7 @@ interface SearchApiQueryInterface { * - execution: The actual query to the search server, in whatever form. * - postprocessing: Preparing the results for returning. * Additional metadata may be returned in other keys. Only 'result count' - * and 'result' always have to be set, all other entries are optional. + * and 'results' always have to be set, all other entries are optional. * * @throws SearchApiException * If an error prevented the search from completing. From 0c0083da8ea932c3dfdae66fd4508ed158e3b5a2 Mon Sep 17 00:00:00 2001 From: rakeshjames Date: Fri, 11 Dec 2015 16:39:04 +0100 Subject: [PATCH 183/278] Issue #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with documentation standards. --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 10 +++++++--- includes/processor.inc | 15 +++++++++++---- includes/processor_stopwords.inc | 4 +++- search_api.admin.inc | 3 +++ search_api.api.php | 8 ++++---- search_api.module | 22 +++++++++++----------- 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 41db2c62..fcb280c2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with + documentation standards. - #2613054 by temkin: Fixed the "search-api-index" Drush command to allow setting further options when indexing on all indexes. - #2611726 by Hubbs, rakesh.gectcr: Fixed several typos. diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 8ae612cd..9535be3c 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -269,10 +269,13 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { /** * Helper method for getting all available aggregation types. * - * @param $info (optional) - * One of "name", "type" or "description", to indicate what values should be - * returned for the types. Defaults to "name". + * @param string $info + * (optional) One of "name", "type" or "description", to indicate what + * information should be returned for the types. * + * @return string[] + * An associative array of aggregation type identifiers mapped to their + * names, data types or descriptions, as requested. */ protected function getTypes($info = 'name') { switch ($info) { @@ -310,6 +313,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'), ); } + return array(); } /** diff --git a/includes/processor.inc b/includes/processor.inc index fe92882c..4baedbbd 100644 --- a/includes/processor.inc +++ b/includes/processor.inc @@ -390,13 +390,15 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface } /** + * Determines whether to process data from the given field. + * * @param $name * The field's machine name. * @param array $field * The field's information. * - * @return - * TRUE, iff the field should be processed. + * @return bool + * TRUE, if the field should be processed, FALSE otherwise. */ protected function testField($name, array $field) { if (empty($this->options['fields'])) { @@ -406,8 +408,13 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface } /** - * @return - * TRUE, iff the type should be processed. + * Determines whether fields of the given type should normally be processed. + * + * Defaults to processing text types, but can easily be overridden by + * subclasses. + * + * @return bool + * TRUE, if the type should be processed, FALSE otherwise. */ protected function testType($type) { return search_api_is_text_type($type, array('text', 'tokens')); diff --git a/includes/processor_stopwords.inc b/includes/processor_stopwords.inc index dddb7969..00918716 100644 --- a/includes/processor_stopwords.inc +++ b/includes/processor_stopwords.inc @@ -86,7 +86,9 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { } /** - * @return + * Retrieves the processor's configured stopwords. + * + * @return array * An array whose keys are the stopwords set in either the file or the text * field. */ diff --git a/search_api.admin.inc b/search_api.admin.inc index a0d935c9..c2140366 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -2102,6 +2102,9 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp * @param array $variables * An associative array containing: * - element: A render element representing the form. + * + * @return string + * The HTML for a field list form. */ function theme_search_api_admin_fields_table($variables) { $form = $variables['element']; diff --git a/search_api.api.php b/search_api.api.php index e5c2ca62..d1740c6d 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -584,15 +584,15 @@ function hook_default_search_api_index_alter(array &$defaults) { * This function will be called for fields of the specific data type to convert * all individual values of the field to the correct format. * - * @param $value + * @param mixed $value * The raw, single value, as extracted from an entity wrapper. - * @param $original_type + * @param string $original_type * The original Entity API type of the value. - * @param $type + * @param string $type * The custom data type to which the value should be converted. Can be ignored * if the callback is only used for a single data type. * - * @return + * @return mixed|null * The converted value, if a conversion could be executed. NULL otherwise. * * @see hook_search_api_data_type_info() diff --git a/search_api.module b/search_api.module index 91a0d00e..1e4fb6a1 100644 --- a/search_api.module +++ b/search_api.module @@ -2100,8 +2100,8 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu * @param array $allowed * Optionally, an array of allowed types. * - * @return - * TRUE if $type is either one of the specified types, or a list of such + * @return bool + * TRUE if $type is either one of the specified types or a list of such * values. FALSE otherwise. * * @see search_api_extract_inner_type() @@ -2117,7 +2117,7 @@ function search_api_is_text_type($type, array $allowed = array('text')) { * @param $type * A string containing the type to check. * - * @return + * @return bool * TRUE iff $type is a list type ("list<*>"). */ function search_api_is_list_type($type) { @@ -2130,7 +2130,7 @@ function search_api_is_list_type($type) { * @param $type * A string containing the type to check. * - * @return + * @return int * The nesting level of the type. 0 for singular types, 1 for lists of * singular types, etc. */ @@ -2157,7 +2157,7 @@ function search_api_list_nesting_level($type) { * @param $nested_type * Another type, determining the nesting level. * - * @return + * @return string * A list version of $type, as specified above. */ function search_api_nest_type($type, $nested_type) { @@ -2174,7 +2174,7 @@ function search_api_nest_type($type, $nested_type) { * @param $type * A string containing the list type to process. * - * @return + * @return string * A string containing the primitive type contained within the list, e.g. * "text" for "list" (or for "list>"). If $type is no list * type, it is returned unchanged. @@ -2453,7 +2453,7 @@ function search_api_access_disable_page(Entity $entity) { * @param Entity $entity * The server or index for which the access to the delete page is checked. * - * @return + * @return bool * TRUE if the delete page can be accessed by the user, FALSE otherwise. */ function search_api_access_delete_page(Entity $entity) { @@ -2568,7 +2568,7 @@ function search_api_server_clear($id) { * @param $id * The ID or machine name of the server to delete. * - * @return + * @return int|false * 1 on success, 0 or FALSE on failure. */ function search_api_server_delete($id) { @@ -2779,7 +2779,7 @@ function search_api_index_disable($id) { * @param $id * The ID or machine name of the index to re-index. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ function search_api_index_reindex($id) { @@ -2803,7 +2803,7 @@ function _search_api_index_reindex(SearchApiIndex $index) { * @param $id * The ID or machine name of the index to clear. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ function search_api_index_clear($id) { @@ -2817,7 +2817,7 @@ function search_api_index_clear($id) { * @param $id * The ID or machine name of the index to delete. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ function search_api_index_delete($id) { From 94ad069e56df8f5f0635789488f687c8c222ca51 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 12 Dec 2015 13:54:55 +0100 Subject: [PATCH 184/278] Issue #2572487 by drunken monkey: Removed operator setting for date facets. --- CHANGELOG.txt | 1 + contrib/search_api_facetapi/plugins/facetapi/adapter.inc | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fcb280c2..16160e7b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2572487 by drunken monkey: Removed operator setting for date facets. - #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with documentation standards. - #2613054 by temkin: Fixed the "search-api-index" Drush command to allow diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index c9855b71..a10db9c0 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -271,6 +271,9 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { '#options' => $granularity_options, '#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE, ); + + // Date facets don't support the "OR" operator (for now). + $form['global']['operator']['#access'] = FALSE; } // Add an "Exclude" option for terms. From 4c947732f1ec103e3ff1823824b697057b718b9c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 12 Dec 2015 13:57:50 +0100 Subject: [PATCH 185/278] Issue #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/handler_filter_fulltext.inc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 16160e7b..f8cb0e44 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field. - #2572487 by drunken monkey: Removed operator setting for date facets. - #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with documentation standards. diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index 71745aea..de61c9bf 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -144,7 +144,8 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex return; } $fields = $this->options['fields']; - $fields = $fields ? $fields : array_keys($this->getFulltextFields()); + $available_fields = array_keys($this->getFulltextFields()); + $fields = $fields ? array_intersect($fields, $available_fields) : $available_fields; // If something already specifically set different fields, we silently fall // back to mere filtering. From 52d9bb50b4282b57807fccd59e8a0679d1b4ca31 Mon Sep 17 00:00:00 2001 From: tauno Date: Sat, 9 Jan 2016 13:05:59 +0100 Subject: [PATCH 186/278] Issue #2631276 by tauno: Fixed the MLT handler for multi-entity indexes. --- CHANGELOG.txt | 1 + .../handler_argument_more_like_this.inc | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f8cb0e44..7f65650c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2631276 by tauno: Fixed the MLT handler for multi-entity indexes. - #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field. - #2572487 by drunken monkey: Removed operator setting for date facets. - #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with diff --git a/contrib/search_api_views/includes/handler_argument_more_like_this.inc b/contrib/search_api_views/includes/handler_argument_more_like_this.inc index 9708851c..df526e4b 100644 --- a/contrib/search_api_views/includes/handler_argument_more_like_this.inc +++ b/contrib/search_api_views/includes/handler_argument_more_like_this.inc @@ -18,6 +18,7 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg $options = parent::option_definition(); unset($options['break_phrase']); unset($options['not']); + $options['entity_type'] = array('default' => FALSE); $options['fields'] = array('default' => array()); return $options; } @@ -31,6 +32,20 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg unset($form['not']); $index = search_api_index_load(substr($this->table, 17)); + + if ($index->datasource() instanceof SearchApiCombinedEntityDataSourceController) { + $types = array_intersect_key(search_api_entity_type_options_list(), array_flip($index->options['datasource']['types'])); + $form['entity_type'] = array( + '#type' => 'select', + '#title' => t('Entity type'), + '#description' => t('Select the entity type of the argument.'), + '#options' => $types, + '#default_value' => $this->options['entity_type'], + '#required' => TRUE, + ); + } + + if (!empty($index->options['fields'])) { $fields = array(); foreach ($index->getFields() as $key => $field) { @@ -78,8 +93,15 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg else { $fields = array_intersect($this->options['fields'], $index_fields); } + if ($this->query->getIndex()->datasource() instanceof SearchApiCombinedEntityDataSourceController) { + $id = $this->options['entity_type'] . '/' . $this->argument; + } + else { + $id = $this->argument; + } + $mlt = array( - 'id' => $this->argument, + 'id' => $id, 'fields' => $fields, ); $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt); From 8da43466a0dcb787befe09f1f4b3b551c28aef9b Mon Sep 17 00:00:00 2001 From: "kraynuk.m" Date: Sat, 9 Jan 2016 14:56:56 +0100 Subject: [PATCH 187/278] Issue #2569461 by kraynuk.m, drunken monkey: Fixed existing table in update #7118. --- CHANGELOG.txt | 1 + search_api.install | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7f65650c..bb213cdf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2569461 by kraynuk.m, drunken monkey: Fixed existing table in update #7118. - #2631276 by tauno: Fixed the MLT handler for multi-entity indexes. - #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field. - #2572487 by drunken monkey: Removed operator setting for date facets. diff --git a/search_api.install b/search_api.install index 3650a033..34682d9c 100644 --- a/search_api.install +++ b/search_api.install @@ -1035,6 +1035,13 @@ function search_api_update_7117() { * Adds the {search_api_item_string_id} table for items with string IDs. */ function search_api_update_7118() { + // Some users have reported that the table already existed for them, for + // whatever reason. Therefore, just bail if the table already exists, assuming + // it already looks as expected. + if (db_table_exists('search_api_item_string_id')) { + return; + } + $table = array( 'description' => 'Stores the items which should be indexed for each index, and their status. Used only for items with string IDs.', 'fields' => array( From 9a97857a5b0537569edee76e8b96b9e3a822f0b2 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 10 Feb 2016 19:55:54 +0100 Subject: [PATCH 188/278] Issue #2629136 by drunken monkey, deranga: Fixed wrong facet counts in edge cases for active OR facets. --- CHANGELOG.txt | 2 ++ .../search_api_facetapi/plugins/facetapi/query_type_term.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bb213cdf..027d39eb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2629136 by drunken monkey, deranga: Fixed wrong facet counts in edge cases + for active OR facets. - #2569461 by kraynuk.m, drunken monkey: Fixed existing table in update #7118. - #2631276 by tauno: Fixed the MLT handler for multi-entity indexes. - #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index ddc119d0..d17721f4 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -174,7 +174,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // Always include the active facet items. foreach ($this->adapter->getActiveItems($this->facet) as $filter) { - $build[$filter['value']]['#count'] = $results['result count']; + $build[$filter['value']]['#count'] = 0; } // Then, add the facets returned by the server. From 39e9e8651401bd6f241c9ef828bf5366a2b2f664 Mon Sep 17 00:00:00 2001 From: joachim Date: Mon, 15 Feb 2016 12:42:35 +0100 Subject: [PATCH 189/278] Issue #2638740 by joachim, drunken monkey: Added a link to the index to the "re-indexing necessary" message. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 027d39eb..db5b4789 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2638740 by joachim, drunken monkey: Added a link to the index to the + "re-indexing necessary" message. - #2629136 by drunken monkey, deranga: Fixed wrong facet counts in edge cases for active OR facets. - #2569461 by kraynuk.m, drunken monkey: Fixed existing table in update #7118. diff --git a/search_api.admin.inc b/search_api.admin.inc index c2140366..37f48566 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -2150,6 +2150,7 @@ function theme_search_api_admin_fields_table($variables) { function search_api_admin_index_fields_submit(array $form, array &$form_state) { $index = $form_state['index']; $options = isset($index->options) ? $index->options : array(); + $index_path = 'admin/config/search/search_api/index/' . $index->machine_name; if ($form_state['values']['op'] == t('Save changes')) { $fields = $form_state['values']['fields']; $default_types = search_api_default_field_types(); @@ -2179,18 +2180,18 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { $ret = $index->update(array('options' => $options)); if ($ret) { - drupal_set_message(t('The indexed fields were successfully changed. ' . - 'The index was cleared and will have to be re-indexed with the new settings.')); + $vars = array('@url' => $index_path); + drupal_set_message(t('The indexed fields were successfully changed. The index was cleared and will have to be re-indexed with the new settings.', $vars)); } else { drupal_set_message(t('No values were changed.')); } if (isset($index->options['data_alter_callbacks']) || isset($index->options['processors'])) { - $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields'; + $form_state['redirect'] = $index_path . '/fields'; } else { drupal_set_message(t('Please set up the indexing workflow.')); - $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow'; + $form_state['redirect'] = $index_path . '/workflow'; } return; } @@ -2205,7 +2206,7 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { else { drupal_set_message(t('No values were changed.')); } - $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields'; + $form_state['redirect'] = $index_path . '/fields'; } /** From 9fa6ac3665d8c0599cadef25ad9b895a8df30d7d Mon Sep 17 00:00:00 2001 From: joachim Date: Mon, 15 Feb 2016 13:44:31 +0100 Subject: [PATCH 190/278] Issue #2639200 by joachim: Added sorting to "related fields" select box. --- CHANGELOG.txt | 1 + search_api.admin.inc | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index db5b4789..63193b77 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2639200 by joachim: Added sorting to "related fields" select box. - #2638740 by joachim, drunken monkey: Added a link to the index to the "re-indexing necessary" message. - #2629136 by drunken monkey, deranga: Fixed wrong facet counts in edge cases diff --git a/search_api.admin.inc b/search_api.admin.inc index 37f48566..8c445055 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1949,6 +1949,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp ); if ($additional) { + asort($additional); reset($additional); $form['additional'] = array( '#type' => 'fieldset', From 4297217dee8771aa6d5668b2d53bac5db8e5fdea Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 23 Feb 2016 14:47:50 +0100 Subject: [PATCH 191/278] Issue #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">" for open facet ranges. --- CHANGELOG.txt | 2 ++ contrib/search_api_facetapi/search_api_facetapi.module | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 63193b77..6efa1d45 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">" for open + facet ranges. - #2639200 by joachim: Added sorting to "related fields" select box. - #2638740 by joachim, drunken monkey: Added a link to the index to the "re-indexing necessary" message. diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index b7558be4..bb3c8b24 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -340,13 +340,13 @@ function search_api_facetapi_facet_map_callback(array $values, array $options = $lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower']; $upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper']; if ($lower == '*' && $upper == '*') { - $map[$value] = t('any'); + $map[$value] = t('any'); } elseif ($lower == '*') { - $map[$value] = "< $upper"; + $map[$value] = "≤ $upper"; } elseif ($upper == '*') { - $map[$value] = "> $lower"; + $map[$value] = "≥ $lower"; } else { $map[$value] = "$lower – $upper"; From db053d958765a5b394e06888a62f987b7fdb16da Mon Sep 17 00:00:00 2001 From: lesmana Date: Fri, 26 Feb 2016 15:53:43 +0100 Subject: [PATCH 192/278] Issue #2667872 by Les Lim: Added "0" to field boost options. --- CHANGELOG.txt | 1 + search_api.admin.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6efa1d45..04ad2fe0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2667872 by Les Lim: Added "0" to field boost options. - #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">" for open facet ranges. - #2639200 by joachim: Added sorting to "related fields" select box. diff --git a/search_api.admin.inc b/search_api.admin.inc index 8c445055..2167e787 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1806,7 +1806,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp // An array of option arrays for types, keyed by nesting level. $types = array(0 => search_api_field_types()); $entity_types = entity_get_info(); - $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0')); + $boosts = drupal_map_assoc(array('0.0', '0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0')); $fulltext_types = array(0 => array('text')); // Add all custom data types with fallback "text" to fulltext types as well. From d88b02d9a562fe07272f464e9181a4b037c8c30c Mon Sep 17 00:00:00 2001 From: "stefan.r" Date: Mon, 14 Mar 2016 11:08:52 +0100 Subject: [PATCH 193/278] Issue #2678856 by stefan.r, drunken monkey: Fixed date facets showing wrong month on certain days. --- CHANGELOG.txt | 2 ++ contrib/search_api_facetapi/search_api_facetapi.module | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 04ad2fe0..f8bd6bb3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2678856 by stefan.r, drunken monkey: Fixed date facets showing wrong month + on certain days. - #2667872 by Les Lim: Added "0" to field boost options. - #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">" for open facet ranges. diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index bb3c8b24..cf78b7bd 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -541,7 +541,9 @@ function search_api_facetapi_map_date(array $values, array $options = array()) { // Otherwise, parse the timestamp from the known format and format it as a // label. $format = search_api_facetapi_date_get_granularity_format($granularity); - $date = DateTime::createFromFormat($format, $value); + // Use the "!" modifier to make the date parsing independent of the current + // date/time. (See #2678856.) + $date = DateTime::createFromFormat('!' . $format, $value); if (!$date) { continue; } From ea7e6f68356eef93ed9e66da7fe3b3701376cbcc Mon Sep 17 00:00:00 2001 From: "stefan.r" Date: Mon, 14 Mar 2016 11:13:07 +0100 Subject: [PATCH 194/278] Issue #2677900 by stefan.r, drunken monkey: Added the possibility to change date facet formats. --- CHANGELOG.txt | 2 + .../search_api_facetapi.install | 32 +++++++- .../search_api_facetapi.module | 75 +++++++++++++++---- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f8bd6bb3..f2210c62 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2677900 by stefan.r, drunken monkey: Added the possibility to change date + facet formats. - #2678856 by stefan.r, drunken monkey: Fixed date facets showing wrong month on certain days. - #2667872 by Les Lim: Added "0" to field boost options. diff --git a/contrib/search_api_facetapi/search_api_facetapi.install b/contrib/search_api_facetapi/search_api_facetapi.install index a626a0cd..5743e080 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.install +++ b/contrib/search_api_facetapi/search_api_facetapi.install @@ -5,9 +5,39 @@ * Install, update and uninstall functions for the Search facets module. */ +/** + * Implements hook_install(). + */ +function search_api_facetapi_install() { + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR, 'Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH, 'F Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY, 'F j, Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR, 'H:__'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE, 'H:i'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND, 'H:i:S'); +} + /** * Implements hook_uninstall(). */ function search_api_facetapi_uninstall() { variable_del('search_api_facets_search_ids'); -} \ No newline at end of file + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR); + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH); + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY); + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR); + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE); + variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND); +} + +/** + * Set up date formats. + */ +function search_api_facetapi_update_7101() { + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR, 'Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH, 'F Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY, 'F j, Y'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR, 'H:__'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE, 'H:i'); + variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND, 'H:i:S'); +} diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module index cf78b7bd..3696eae5 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.module +++ b/contrib/search_api_facetapi/search_api_facetapi.module @@ -211,6 +211,58 @@ function search_api_facetapi_search_api_query_alter($query) { } } +/** + * Implements hook_date_formats(). + */ +function search_api_facetapi_date_formats() { + return array( + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_YEAR, + 'format' => 'Y', + 'locales' => array(), + ), + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_MONTH, + 'format' => 'F Y', + 'locales' => array(), + ), + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_DAY, + 'format' => 'F j, Y', + 'locales' => array(), + ), + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_HOUR, + 'format' => 'H:__', + 'locales' => array(), + ), + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_MINUTE, + 'format' => 'H:i', + 'locales' => array(), + ), + array( + 'type' => 'search_api_facetapi_' . FACETAPI_DATE_SECOND, + 'format' => 'H:i:s', + 'locales' => array(), + ), + ); +} + +/** + * Implements hook_date_format_types(). + */ +function search_api_facetapi_date_format_types() { + return array( + 'search_api_facetapi_' . FACETAPI_DATE_YEAR => t('Search facets - Years'), + 'search_api_facetapi_' . FACETAPI_DATE_MONTH => t('Search facets - Months'), + 'search_api_facetapi_' . FACETAPI_DATE_DAY => t('Search facets - Days'), + 'search_api_facetapi_' . FACETAPI_DATE_HOUR => t('Search facets - Hours'), + 'search_api_facetapi_' . FACETAPI_DATE_MINUTE => t('Search facets - Minutes'), + 'search_api_facetapi_' . FACETAPI_DATE_SECOND => t('Search facets - Seconds'), + ); +} + /** * Menu callback for the facet settings page. */ @@ -532,12 +584,6 @@ function search_api_facetapi_map_date(array $values, array $options = array()) { continue; } - // For years, the URL value is already the label. - if ($granularity == FACETAPI_DATE_YEAR) { - $map[$value] = $value; - continue; - } - // Otherwise, parse the timestamp from the known format and format it as a // label. $format = search_api_facetapi_date_get_granularity_format($granularity); @@ -570,17 +616,16 @@ function search_api_facetapi_map_date(array $values, array $options = array()) { */ function search_api_facetapi_format_timestamp($timestamp, $precision = FACETAPI_DATE_YEAR) { $formats = array( - FACETAPI_DATE_YEAR => 'Y', - FACETAPI_DATE_MONTH => 'F Y', - FACETAPI_DATE_DAY => 'F j, Y', - FACETAPI_DATE_HOUR => 'H:__', - FACETAPI_DATE_MINUTE => 'H:i', - FACETAPI_DATE_SECOND => 'H:i:s', + FACETAPI_DATE_YEAR, + FACETAPI_DATE_MONTH, + FACETAPI_DATE_DAY, + FACETAPI_DATE_HOUR, + FACETAPI_DATE_MINUTE, + FACETAPI_DATE_SECOND, ); - if (!isset($formats[$precision])) { + if (!in_array($precision, $formats)) { $precision = FACETAPI_DATE_YEAR; } - - return format_date($timestamp, 'custom', $formats[$precision]); + return format_date($timestamp, 'search_api_facetapi_' . $precision); } From 8b1bcf114d887504e7af53c25b36760197eb759b Mon Sep 17 00:00:00 2001 From: recrit Date: Mon, 14 Mar 2016 12:52:24 +0100 Subject: [PATCH 195/278] Issue #2665586 by recrit, drunken monkey: Fixed parsing of invalid date facet filters. --- CHANGELOG.txt | 2 + .../plugins/facetapi/query_type_date.inc | 67 ++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f2210c62..a5a7b4cc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2665586 by recrit, drunken monkey: Fixed parsing of invalid date facet + filters. - #2677900 by stefan.r, drunken monkey: Added the possibility to change date facet formats. - #2678856 by stefan.r, drunken monkey: Fixed date facets showing wrong month diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 8a565302..4042fdfd 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -58,27 +58,36 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue $item = end($active); $field = $this->facet['field']; $filter = $this->createRangeFilter($item['value']); - $this->addFacetFilter($query, $field, $filter); + if ($filter) { + $this->addFacetFilter($query, $field, $filter); + } } } /** * Rewrites the handler-specific date range syntax to the normal facet syntax. * - * @param $value + * @param string $value * The user-facing facet value. * - * @return string + * @return string|null * A facet to add as a filter, in the format used internally in this module. + * Or NULL if the raw facet in $value is not valid. */ protected function createRangeFilter($value) { - // Gets the granularity. Ignore any filters passed directly from the server - // (range or missing). We always create filters starting with a year. - if (!$value || !ctype_digit($value[0])) { - return $value; + // Ignore any filters passed directly from the server (range or missing). + if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ [^ ]+[])]$/', $value))) { + return $value ? $value : NULL; + } + + // Parse into date parts. + $parts = $this->parseRangeFilter($value); + + // Return NULL if the date parts are invalid or none were found. + if (empty($parts)) { + return NULL; } - $parts = explode('-', $value); $date = new DateTime(); switch (count($parts)) { case 1: @@ -140,6 +149,48 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue return "[$lower TO $upper]"; } + /** + * Parses the date range filter value into parts. + * + * @param string $value + * The user-facing facet value. + * + * @return int[]|null + * An array of date parts, or NULL if an invalid value was provided. + */ + protected static function parseRangeFilter($value) { + $parts = explode('-', $value); + + foreach ($parts as $i => $part) { + // Invalidate if part is not an integer. + if ($part === '' || !is_numeric($part) || intval($part) != $part) { + return NULL; + } + $parts[$i] = (int) $part; + // Depending on the position, negative numbers or 0 are invalid. + switch ($i) { + case 0: + // Years can contain anything – negative values are unlikely, but + // technically possible. + break; + case 1: + case 2: + // Days and months have to be positive. + if ($part <= 0) { + return NULL; + } + break; + default: + // All others can be 0, but not negative. + if ($part < 0) { + return NULL; + } + } + } + + return $parts; + } + /** * Replacement callback for replacing ISO dates with timestamps. * From 16634386d62fb557ec1b07fc16ca49d8aa22e868 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 14 Mar 2016 16:08:48 +0100 Subject: [PATCH 196/278] Adapted CHANGELOG.txt to reflect the new 1.17 release. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a5a7b4cc..2aa7f5d5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,8 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- + +Search API 1.17 (2016-03-14): +----------------------------- - #2665586 by recrit, drunken monkey: Fixed parsing of invalid date facet filters. - #2677900 by stefan.r, drunken monkey: Added the possibility to change date From 00795c1b7ace80c16b7bd49777453c067c501a60 Mon Sep 17 00:00:00 2001 From: git Date: Thu, 24 Mar 2016 10:02:25 +0100 Subject: [PATCH 197/278] Issue #2693425 by jojyja: Fixed a typo in search_api.info. --- CHANGELOG.txt | 1 + search_api.info | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2aa7f5d5..94ccad26 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2693425 by jojyja: Fixed a typo in search_api.info. Search API 1.17 (2016-03-14): ----------------------------- diff --git a/search_api.info b/search_api.info index 2c4ba14c..06cb0242 100644 --- a/search_api.info +++ b/search_api.info @@ -1,5 +1,5 @@ name = Search API -description = "Provides a generic API for modules offering search capabilites." +description = "Provides a generic API for modules offering search capabilities." dependencies[] = entity core = 7.x package = Search From f362e5b86a17919d0b60b9ef1c5c7c7858fb4f97 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 4 Apr 2016 16:10:22 +0200 Subject: [PATCH 198/278] Added hook group info for three more hooks. --- search_api.module | 3 +++ 1 file changed, 3 insertions(+) diff --git a/search_api.module b/search_api.module index 1e4fb6a1..15cc522b 100644 --- a/search_api.module +++ b/search_api.module @@ -216,10 +216,13 @@ function search_api_hook_info() { 'search_api_data_type_info' => $hook_info, 'search_api_data_type_info_alter' => $hook_info, 'search_api_alter_callback_info' => $hook_info, + 'search_api_alter_callback_info_alter' => $hook_info, 'search_api_processor_info' => $hook_info, + 'search_api_processor_info_alter' => $hook_info, 'search_api_index_items_alter' => $hook_info, 'search_api_items_indexed' => $hook_info, 'search_api_query_alter' => $hook_info, + 'search_api_results_alter' => $hook_info, 'search_api_server_load' => $hook_info, 'search_api_server_insert' => $hook_info, 'search_api_server_update' => $hook_info, From 91df7107dd58c209e8e4303ef9bd1533456fbcab Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 20 Apr 2016 10:13:44 +0200 Subject: [PATCH 199/278] Fixed reaction to updating of node access records. --- search_api.module | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/search_api.module b/search_api.module index 15cc522b..473c683d 100644 --- a/search_api.module +++ b/search_api.module @@ -923,6 +923,36 @@ function search_api_entity_delete($entity, $type) { } } +/** + * Implements hook_node_access_records_alter(). + * + * Marks the node as "changed" in indexes that use the "Node access" data + * alteration. Also marks the node's comments as changed in indexes that use the + * "Comment access" data alteration. + */ +function search_api_node_access_records_alter(&$grants, $node) { + foreach (search_api_index_load_multiple(FALSE) as $index) { + $item_ids = array(); + if (!empty($index->options['data_alter_callbacks']['search_api_alter_node_access']['status'])) { + $item_id = $index->datasource()->getItemId($node); + $item_ids = array($item_id); + } + elseif (!empty($index->options['data_alter_callbacks']['search_api_alter_comment_access']['status'])) { + if (!isset($comments)) { + $comments = comment_load_multiple(FALSE, array('nid' => $node->nid)); + } + foreach ($comments as $comment) { + $item_ids[] = $index->datasource()->getItemId($comment); + } + } + + if ($item_ids) { + $indexes = array($index->machine_name => $index); + search_api_track_item_change_for_indexes($index->item_type, $item_ids, $indexes); + } + } +} + /** * Implements hook_field_attach_rename_bundle(). * @@ -1189,6 +1219,20 @@ function search_api_track_item_change($type, array $item_ids) { if (!$indexes) { return; } + search_api_track_item_change_for_indexes($type, $item_ids, $indexes); +} + +/** + * Marks the items with the specified IDs as "dirty" for the given indexes. + * + * @param string $type + * The item type of the items. + * @param array $item_ids + * The item IDs. + * @param SearchApiIndex[] $indexes + * The indexes for which to mark the items as "dirty". + */ +function search_api_track_item_change_for_indexes($type, array $item_ids, $indexes) { try { $returned_indexes = search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes); if (isset($returned_indexes)) { @@ -1210,7 +1254,6 @@ function search_api_track_item_change($type, array $item_ids) { catch (SearchApiException $e) { $vars['%item_type'] = $type; watchdog_exception('search_api', $e, '%type while updating items of type %item_type: !message in %function (line %line of %file).', $vars); - return; } } From cd32d59f3632b80be1374801dea3cccf68088b0c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 20 Apr 2016 10:14:36 +0200 Subject: [PATCH 200/278] Fixed typo when checking for access to comments. --- search_api.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_api.module b/search_api.module index 473c683d..af2c9932 100644 --- a/search_api.module +++ b/search_api.module @@ -2104,7 +2104,7 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu } // If the user cannot access content/comments at all, return no results. - if (!user_access('access content', $account) || ($is_comment && !user_access('access content', $account))) { + if (!user_access('access content', $account) || ($is_comment && !user_access('access comments', $account))) { // Simple hack for returning no results. $query->condition('status', 0); $query->condition('status', 1); From 560e623144a05ba1b4c8e87efa44236abd0c5971 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 20 Apr 2016 14:25:50 +0200 Subject: [PATCH 201/278] Revised mechanism for passing (highlighted) field values to Views. --- contrib/search_api_views/includes/query.inc | 2 +- includes/processor_highlight.inc | 12 +++-- includes/query.inc | 11 +++- search_api.module | 56 +++++++++++++++++++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 958ee7a3..8ac6f2c6 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -444,7 +444,7 @@ class SearchApiViewsQuery extends views_plugin_query { // Gather any fields from the search results. if (!empty($result['fields'])) { - $row['_entity_properties'] += $result['fields']; + $row['_entity_properties'] += search_api_get_sanitized_field_values($result['fields']); } // Check whether we need to extract any properties from the result item. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 566599af..62aa1581 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -158,13 +158,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { if ($this->options['highlight'] != 'never') { $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields, $this->options['highlight'] == 'always'); foreach ($fields as $field => $data) { + $result['fields'][$field] = array('#sanitize_callback' => FALSE); if (is_array($data)) { foreach ($data as $i => $text) { - $result['fields'][$field][$i] = $this->highlightField($text, $keys); + $result['fields'][$field]['#value'][$i] = $this->highlightField($text, $keys); } } else { - $result['fields'][$field] = $this->highlightField($data, $keys); + $result['fields'][$field]['#value'] = $this->highlightField($data, $keys); } } } @@ -200,9 +201,10 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { // We only need detailed fields data if $load is TRUE. $fields = $load ? $this->index->getFields() : array(); $needs_extraction = array(); + $returned_fields = search_api_get_sanitized_field_values(array_intersect_key($result['fields'], array_flip($fulltext_fields))); foreach ($fulltext_fields as $field) { - if (array_key_exists($field, $result['fields'])) { - $data[$field] = $result['fields'][$field]; + if (array_key_exists($field, $returned_fields)) { + $data[$field] = $returned_fields[$field]; } elseif ($load) { $needs_extraction[$field] = $fields[$field]; @@ -225,7 +227,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { } $wrapper = $this->index->entityWrapper($result['entity'], FALSE); $wrapper->language($language->language); - $extracted = search_api_extract_fields($wrapper, $needs_extraction); + $extracted = search_api_extract_fields($wrapper, $needs_extraction, array('sanitize' => TRUE)); foreach ($extracted as $field => $info) { if (isset($info['value'])) { diff --git a/includes/query.inc b/includes/query.inc index 30cd6e9a..debed66a 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -201,7 +201,16 @@ interface SearchApiQueryInterface { * already ready-to-use. This allows search engines (or postprocessors) * to store extracted fields so other modules don't have to extract them * again. This fields should always be checked by modules that want to - * use field contents of the result items. + * use field contents of the result items. The format of the array is + * field IDs (as used by the Search API internally) mapped to either the + * raw value of the field (scalar or array value), or an associative + * array with the following keys: + * - #value: The raw field value. + * - #sanitize_callback: The callback to use for sanitizing the field + * value for HTML output, or FALSE to state that the field value is + * already sanitized. + * In the simple form, it's assumed the field value should be sanitized + * with check_plain(). * - entity: (optional) If set, the fully loaded result item. This field * should always be used by modules using search results, to avoid * duplicate item loads. diff --git a/search_api.module b/search_api.module index af2c9932..bc2d28c0 100644 --- a/search_api.module +++ b/search_api.module @@ -2875,6 +2875,62 @@ function search_api_index_delete($id) { return TRUE; } +/** + * Sanitizes field values returned from the server. + * + * @param array $values + * The field values, as returned from the server. See + * SearchApiQueryInterface::execute() for documentation on the structure. + * + * @return array + * An associative array of field IDs mapped to their sanitized values (scalar + * or array-valued). + */ +function search_api_get_sanitized_field_values(array $values) { + // Sanitize the field values returned from the server. Usually we use + // check_plain(), but this can be overridden by setting the field value to + // an array with "#value" and "#sanitize_callback" keys. + foreach ($values as $field_id => $field_value) { + if (is_array($field_value) + && isset($field_value['#sanitize_callback']) + && ($field_value['#sanitize_callback'] === FALSE || is_callable($field_value['#sanitize_callback'])) + && array_key_exists('#value', $field_value) + ) { + $sanitize_callback = $field_value['#sanitize_callback']; + $field_value = $field_value['#value']; + } + else { + $sanitize_callback = 'check_plain'; + } + if ($sanitize_callback !== FALSE) { + $field_value = search_api_sanitize_field_value($field_value, $sanitize_callback); + } + $values[$field_id] = $field_value; + } + return $values; +} + +/** + * Sanitizes the given field value(s). + * + * @param mixed $field_value + * A scalar field value, or an array of field values. + * @param callable $sanitize_callback + * (optional) The callback to use for sanitizing a scalar value. + * + * @return mixed + * The sanitized field value(s). + */ +function search_api_sanitize_field_value($field_value, $sanitize_callback = 'check_plain') { + if (is_scalar($field_value)) { + return call_user_func($sanitize_callback, $field_value); + } + foreach ($field_value as &$nested_value) { + $nested_value = search_api_sanitize_field_value($nested_value, $sanitize_callback); + } + return $field_value; +} + /** * Options list callback for search indexes. * From 33f2976f2386b162cbe60e67f578c7e615b3b3d1 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Apr 2016 12:09:28 +0200 Subject: [PATCH 202/278] Adapted CHANGELOG.txt to release 1.18. --- CHANGELOG.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 94ccad26..bfebc913 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,9 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- + +Search API 1.18 (2016-04-20): +----------------------------- +- Various security fixes – see https://www.drupal.org/node/2710063. - #2693425 by jojyja: Fixed a typo in search_api.info. Search API 1.17 (2016-03-14): From 0580e940bc6abbcd3fb58882e39ebe1fcf530e7c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Apr 2016 12:10:06 +0200 Subject: [PATCH 203/278] Issue #2419853 by drunken monkey: Fixed HTML filter leaves escaped entities in field values sometimes. --- CHANGELOG.txt | 2 ++ includes/processor_html_filter.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bfebc913..19e4ddb0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2419853 by drunken monkey: Fixed HTML filter leaves escaped entities in + field values sometimes. Search API 1.18 (2016-04-20): ----------------------------- diff --git a/includes/processor_html_filter.inc b/includes/processor_html_filter.inc index 180c9c18..891b6601 100644 --- a/includes/processor_html_filter.inc +++ b/includes/processor_html_filter.inc @@ -101,7 +101,7 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { $value = $this->parseText($text); } else { - $value = strip_tags($text); + $value = html_entity_decode(strip_tags($text)); // Remove any multiple or leading/trailing spaces we might have introduced. $value = preg_replace('/\s\s+/', ' ', trim($value)); } From 2b336740dc00bf9e07d162d8ace7c88d388d90ab Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Apr 2016 12:29:14 +0200 Subject: [PATCH 204/278] Issue #2703675 by drunken monkey, heykarthikwithu: Fixed accidental assumption that all facets are taxonomy terms. --- CHANGELOG.txt | 2 ++ .../plugins/facetapi/query_type_term.inc | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 19e4ddb0..99b639c1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2703675 by drunken monkey, heykarthikwithu: Fixed accidental assumption that + all facets are taxonomy terms. - #2419853 by drunken monkey: Fixed HTML filter leaves escaped entities in field values sometimes. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index d17721f4..4fd7906c 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -57,21 +57,27 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // When the operator is OR, remove parent terms from the active ones if // children are active. If we don't do this, sending a term and its // parent will produce the same results as just sending the parent. - if ($settings['flatten'] == '0') { + if (is_callable($this->facet['hierarchy callback']) && !$settings['flatten']) { // Check the filters in reverse order, to avoid checking parents that // will afterwards be removed anyways. - foreach (array_reverse(array_keys($active)) as $filter) { + $values = array_keys($active); + $parents = call_user_func($this->facet['hierarchy callback'], $values); + foreach (array_reverse($values) as $filter) { // Skip this filter if it was already removed, or if it is the // "missing value" filter ("!"). if (!isset($active[$filter]) || !is_numeric($filter)) { continue; } - $parents = taxonomy_get_parents_all($filter); - // The return value of taxonomy_get_parents_all() includes the term - // itself at index 0. Remove that to only get the term's ancestors. - unset($parents[0]); - foreach ($parents as $parent) { - unset($active[$parent->tid]); + // Go through the entire hierarchy of the value and remove all its + // ancestors. + while (!empty($parents[$filter])) { + $ancestor = array_shift($parents[$filter]); + if (isset($active[$ancestor])) { + unset($active[$ancestor]); + if (!empty($parents[$ancestor])) { + $parents[$filter] = array_merge($parents[$filter], $parents[$ancestor]); + } + } } } } From 965bea179ab630f2ccdff86e02a3f20c2d3e15e8 Mon Sep 17 00:00:00 2001 From: git Date: Thu, 21 Apr 2016 20:22:08 +0200 Subject: [PATCH 205/278] Issue #2665970 by andrei.colesnic, drunken monkey: Added "Limit list to selected items" exposed option support for Views taxonomy term filters. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_entity.inc | 2 +- .../includes/handler_filter_taxonomy_term.inc | 33 ++++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 99b639c1..dc623050 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2665970 by andrei.colesnic, drunken monkey: Added "Limit list to selected + items" exposed option support for Views taxonomy term filters. - #2703675 by drunken monkey, heykarthikwithu: Fixed accidental assumption that all facets are taxonomy terms. - #2419853 by drunken monkey: Fixed HTML filter leaves escaped entities in diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index ffae8ad4..8d2fe938 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -73,7 +73,7 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi public function option_definition() { $options = parent::option_definition(); - $options['expose']['multiple']['default'] = TRUE; + $options['expose']['contains']['multiple']['default'] = TRUE; return $options; } diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index efa685af..4ee1fba8 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -27,6 +27,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select'); $options['hierarchy'] = array('default' => 0); + $options['expose']['contains']['reduce'] = array('default' => FALSE); $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); return $options; @@ -229,6 +230,14 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte parent::exposed_validate($form, $form_state); } + /** + * {@inheritdoc} + */ + public function expose_options() { + parent::expose_options(); + $this->options['expose']['reduce'] = FALSE; + } + /** * {@inheritdoc} */ @@ -282,15 +291,23 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte */ public function expose_form(&$form, &$form_state) { parent::expose_form($form, $form_state); - if ($this->options['type'] != 'select') { - unset($form['expose']['reduce']); + + if ($this->options['type'] == 'select') { + $form['expose']['reduce'] = array( + '#type' => 'checkbox', + '#title' => t('Limit list to selected items'), + '#description' => t('If checked, the only items presented to the user will be the ones selected here.'), + '#default_value' => $this->options['expose']['reduce'], + ); + } + else { + $form['error_message'] = array( + '#type' => 'checkbox', + '#title' => t('Display error message'), + '#description' => t('Display an error message if one of the entered terms could not be found.'), + '#default_value' => $this->options['error_message'], + ); } - $form['error_message'] = array( - '#type' => 'checkbox', - '#title' => t('Display error message'), - '#description' => t('Display an error message if one of the entered terms could not be found.'), - '#default_value' => !empty($this->options['error_message']), - ); } /** From 02041b1376bf7c472bbd58a2a69a219756e46975 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Apr 2016 21:46:04 +0200 Subject: [PATCH 206/278] Issue #2700011 by drunken monkey: Fixed compatibility issues of facets from different indexes. --- CHANGELOG.txt | 2 ++ .../plugins/facetapi/adapter.inc | 13 ++++++++++--- .../plugins/facetapi/query_type_date.inc | 5 +++-- .../plugins/facetapi/query_type_term.inc | 5 +++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dc623050..212dbebc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2700011 by drunken monkey: Fixed compatibility issues of facets from + different indexes. - #2665970 by andrei.colesnic, drunken monkey: Added "Limit list to selected items" exposed option support for Views taxonomy term filters. - #2703675 by drunken monkey, heykarthikwithu: Fixed accidental assumption that diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index a10db9c0..6251e32b 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -61,6 +61,10 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { public function initActiveFilters($query) { $search_id = $query->getOption('search id'); $index_id = $this->info['instance']; + // Only act on queries from the right index. + if ($index_id != $query->getIndex()->machine_name) { + return; + } $facets = facetapi_get_enabled_facets($this->info['name']); $this->fields = array(); @@ -83,13 +87,16 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { if (array_search($search_id, $facet_search_ids) === FALSE) { if (!$default_true) { - continue; // We are only to show facets for explicitly named search ids. + // We are only to show facets for explicitly named search ids. + continue; } } elseif ($default_true) { - continue; // The 'facet_search_ids' in the settings are to be excluded. + // The 'facet_search_ids' in the settings are to be excluded. + continue; } - $active[$facet['name']] = $search_id; + $facet_key = $facet['name'] . '@' . $this->getSearcher(); + $active[$facet_key] = $search_id; $this->fields[$facet['name']] = array( 'field' => $facet['field'], 'limit' => $options['hard_limit'], diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 4042fdfd..6741cd81 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -210,10 +210,11 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue public function build() { $facet = $this->adapter->getFacet($this->facet); $search_ids = drupal_static('search_api_facetapi_active_facets', array()); - if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) { + $facet_key = $facet['name'] . '@' . $this->adapter->getSearcher(); + if (empty($search_ids[$facet_key]) || !search_api_current_search($search_ids[$facet_key])) { return array(); } - $search_id = $search_ids[$facet['name']]; + $search_id = $search_ids[$facet_key]; $build = array(); $search = search_api_current_search($search_id); $results = $search[1]; diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 4fd7906c..64d797a7 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -171,10 +171,11 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy // initActiveFilters) so that we can retrieve it here and get the correct // current search for this facet. $search_ids = drupal_static('search_api_facetapi_active_facets', array()); - if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) { + $facet_key = $facet['name'] . '@' . $this->adapter->getSearcher(); + if (empty($search_ids[$facet_key]) || !search_api_current_search($search_ids[$facet_key])) { return array(); } - $search_id = $search_ids[$facet['name']]; + $search_id = $search_ids[$facet_key]; list(, $results) = search_api_current_search($search_id); $build = array(); From 7a0b612421d7c0031d4c0de5aaae6de7274a76e2 Mon Sep 17 00:00:00 2001 From: git Date: Thu, 21 Apr 2016 22:01:06 +0200 Subject: [PATCH 207/278] Issue #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to recognize all valid HTML tags. --- CHANGELOG.txt | 2 ++ includes/processor_html_filter.inc | 2 +- search_api.test | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 212dbebc..7f319791 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to recognize all + valid HTML tags. - #2700011 by drunken monkey: Fixed compatibility issues of facets from different indexes. - #2665970 by andrei.colesnic, drunken monkey: Added "Limit list to selected diff --git a/includes/processor_html_filter.inc b/includes/processor_html_filter.inc index 891b6601..0cc4800d 100644 --- a/includes/processor_html_filter.inc +++ b/includes/processor_html_filter.inc @@ -120,7 +120,7 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor { ); } $text = substr($text, $pos + 1); - if (!preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m)) { + if (!preg_match('#^(/?)([:_a-zA-Z][-:_a-zA-Z0-9.]*)#', $text, $m)) { continue; } $text = substr($text, strpos($text, '>') + 1); diff --git a/search_api.test b/search_api.test index 89883028..23f36bcf 100644 --- a/search_api.test +++ b/search_api.test @@ -1062,7 +1062,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { protected function checkHtmlFilter() { $orig = <<a test. +"something">a test.

    Header

    How to write links to other sites: <a href="URL" title="MOUSEOVER TEXT">TEXT</a>. < signs can be escaped with "&lt;". @@ -1071,6 +1071,7 @@ END; $tags = << 'This', 'score' => 1), @@ -1078,6 +1079,7 @@ END; array('value' => 'something', 'score' => 1.5), array('value' => 'a', 'score' => 1.5), array('value' => 'test', 'score' => 1.5), + array('value' => 'Header', 'score' => 3), array('value' => 'How', 'score' => 1), array('value' => 'to', 'score' => 1), array('value' => 'write', 'score' => 1), From 14b90c53007558d705a2a3b977c470bf55aef12b Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 22 Apr 2016 15:07:46 +0200 Subject: [PATCH 208/278] Issue #2700879 by drunken monkey: Fixed breadcrumbs on index tabs. --- CHANGELOG.txt | 1 + search_api.admin.inc | 7 ------- search_api.module | 7 +++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7f319791..b9719884 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2700879 by drunken monkey: Fixed breadcrumbs on index tabs. - #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to recognize all valid HTML tags. - #2700011 by drunken monkey: Fixed compatibility issues of facets from diff --git a/search_api.admin.inc b/search_api.admin.inc index 2167e787..3afbaa78 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -337,13 +337,6 @@ function search_api_admin_add_server_submit(array $form, array &$form_state) { drupal_set_message(t('The server was successfully created.')); } -/** - * Title callback for viewing or editing a server or index. - */ -function search_api_admin_item_title($object) { - return $object->name; -} - /** * Page callback: Displays information about a server. * diff --git a/search_api.module b/search_api.module index bc2d28c0..38d9e138 100644 --- a/search_api.module +++ b/search_api.module @@ -2465,6 +2465,13 @@ function search_api_server_url(SearchApiServer $server) { ); } +/** + * Title callback for viewing or editing a server or index. + */ +function search_api_admin_item_title($object) { + return $object->name; +} + /** * Title callback for determining which title should be displayed for the * "delete" local task. From cfc2bdf320f37d8f56a6cbcd1116df6a80b7a255 Mon Sep 17 00:00:00 2001 From: git Date: Fri, 22 Apr 2016 17:52:25 +0200 Subject: [PATCH 209/278] Issue #2707039 by alan-ps: Fixed indexes of flag entities with "bundles" setting. --- CHANGELOG.txt | 1 + includes/datasource_entity.inc | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b9719884..b501cee3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2707039 by alan-ps: Fixed indexes of flag entities with "bundles" setting. - #2700879 by drunken monkey: Fixed breadcrumbs on index tabs. - #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to recognize all valid HTML tags. diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 034bb525..b8433b83 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -166,6 +166,10 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon $bundle_column = 'vid'; $bundles = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name IN (:bundles)', array(':bundles' => $bundles))->fetchCol(); } + elseif ($this->entityType == 'flagging') { + $bundle_column = 'fid'; + $bundles = db_query('SELECT fid FROM {flag} WHERE name IN (:bundles)', array(':bundles' => $bundles))->fetchCol(); + } elseif ($this->entityType == 'comment') { // Comments are significantly more complicated, since they don't // store their bundle explicitly in their database table. Instead, From 196cd127754e03ca0bbd0246199d252821e9f17d Mon Sep 17 00:00:00 2001 From: git Date: Thu, 5 May 2016 14:02:58 +0200 Subject: [PATCH 210/278] Issue #2710893 by alan-ps, drunken monkey: Fixed creation of comment indexes when no nodes exist. --- CHANGELOG.txt | 2 ++ includes/datasource_entity.inc | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b501cee3..09cee6fd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2710893 by alan-ps, drunken monkey: Fixed creation of comment indexes when + no nodes exist. - #2707039 by alan-ps: Fixed indexes of flag entities with "bundles" setting. - #2700879 by drunken monkey: Fixed breadcrumbs on index tabs. - #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to recognize all diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index b8433b83..3ebbab24 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -186,14 +186,17 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon $bundles = db_query('SELECT nid FROM {node} WHERE type IN (:bundles)', array(':bundles' => $node_types))->fetchCol(); } else { - return; + continue; } } else { $this->startTrackingFallback(array($index->machine_name => $index)); + continue; } } - $query->condition($bundle_column, $bundles); + if ($bundles) { + $query->condition($bundle_column, $bundles); + } } // INSERT ... SELECT ... From d07089b2affc5e92c3b55c09f19f7f3b8545b167 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 7 Jun 2016 13:29:41 +0200 Subject: [PATCH 211/278] Issue #2720465 by drunken monkey: Fixed bundle filter's handling of entity types with no bundles on multi-type indexes. --- CHANGELOG.txt | 2 ++ includes/callback_bundle_filter.inc | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 09cee6fd..26895c05 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2720465 by drunken monkey: Fixed bundle filter's handling of entity types + with no bundles on multi-type indexes. - #2710893 by alan-ps, drunken monkey: Fixed creation of comment indexes when no nodes exist. - #2707039 by alan-ps: Fixed indexes of flag entities with "bundles" setting. diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index 50a9dc9f..ab743d6c 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -34,7 +34,8 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { return; } - if ($this->isMultiEntityIndex()) { + $multi_entity = $this->isMultiEntityIndex(); + if ($multi_entity) { $bundle_prop = 'item_bundle'; } else { @@ -46,6 +47,10 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { $default = (bool) $this->options['default']; foreach ($items as $id => $item) { + // Ignore types that have no bundles. + if ($multi_entity && !self::hasBundles(entity_get_info($item->item_type))) { + continue; + } if (isset($bundles[$item->$bundle_prop]) == $default) { unset($items[$id]); } From 99c23a3e933da785719db600a5162ed9e06f63be Mon Sep 17 00:00:00 2001 From: jsacksick Date: Fri, 10 Jun 2016 17:41:52 +0200 Subject: [PATCH 212/278] Issue #2733447 by jsacksick: Fixed translatability of our Views taxonomy term filter. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_taxonomy_term.inc | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 26895c05..0da5a383 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2733447 by jsacksick: Fixed translatability of our Views taxonomy term + filter. - #2720465 by drunken monkey: Fixed bundle filter's handling of entity types with no bundles on multi-type indexes. - #2710893 by alan-ps, drunken monkey: Fixed creation of comment indexes when diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index 4ee1fba8..b6db4f69 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -73,13 +73,13 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte } else { if ($vocabulary && !empty($this->options['hierarchy'])) { - $tree = taxonomy_get_tree($vocabulary->vid); + $tree = taxonomy_get_tree($vocabulary->vid, 0, NULL, TRUE); $options = array(); if ($tree) { foreach ($tree as $term) { $choice = new stdClass(); - $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); + $choice->option = array($term->tid => str_repeat('-', $term->depth) . check_plain(entity_label('taxonomy_term', $term))); $options[] = $choice; } } @@ -98,8 +98,15 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $query->condition('tv.machine_name', $vocabulary->machine_name); } $result = $query->execute(); + $tids = array(); + foreach ($result as $term) { - $options[$term->tid] = $term->name; + $tids[] = $term->tid; + } + $terms = taxonomy_term_load_multiple($tids); + + foreach ($terms as $term) { + $options[$term->tid] = check_plain(entity_label('taxonomy_term', $term)); } } From ff80f773aa1d33915b1e6a97da13e281549761cc Mon Sep 17 00:00:00 2001 From: tunic Date: Sat, 11 Jun 2016 12:32:52 +0200 Subject: [PATCH 213/278] Issue #2742053 by tunic: Fixed change notification on node access records change. --- CHANGELOG.txt | 1 + search_api.module | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0da5a383..01c25180 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2742053 by tunic: Fixed change notification on node access records change. - #2733447 by jsacksick: Fixed translatability of our Views taxonomy term filter. - #2720465 by drunken monkey: Fixed bundle filter's handling of entity types diff --git a/search_api.module b/search_api.module index 38d9e138..072b0a34 100644 --- a/search_api.module +++ b/search_api.module @@ -935,7 +935,9 @@ function search_api_node_access_records_alter(&$grants, $node) { $item_ids = array(); if (!empty($index->options['data_alter_callbacks']['search_api_alter_node_access']['status'])) { $item_id = $index->datasource()->getItemId($node); - $item_ids = array($item_id); + if ($item_id !== NULL) { + $item_ids = array($item_id); + } } elseif (!empty($index->options['data_alter_callbacks']['search_api_alter_comment_access']['status'])) { if (!isset($comments)) { From d7360f7fff6ad580c89213defba003214e5f7fbd Mon Sep 17 00:00:00 2001 From: johncook Date: Sat, 11 Jun 2016 13:28:27 +0200 Subject: [PATCH 214/278] Issue #2744995 by John Cook, drunken monkey: Fixed search views without pager. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 01c25180..ac9959fd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2744995 by John Cook, drunken monkey: Fixed search views without pager. - #2742053 by tunic: Fixed change notification on node access records change. - #2733447 by jsacksick: Fixed translatability of our Views taxonomy term filter. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 8ac6f2c6..bc9af4de 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -344,13 +344,15 @@ class SearchApiViewsQuery extends views_plugin_query { // FALSE. $skip_result_count = $this->query->getOption('skip result count', TRUE); if ($skip_result_count) { - $skip_result_count = !$this->pager->use_count_query() && empty($view->get_total_rows); + $skip_result_count = !$this->pager || (!$this->pager->use_count_query() && empty($view->get_total_rows)); $this->query->setOption('skip result count', $skip_result_count); } try { // Trigger pager pre_execute(). - $this->pager->pre_execute($this->query); + if ($this->pager) { + $this->pager->pre_execute($this->query); + } // Views passes sometimes NULL and sometimes the integer 0 for "All" in a // pager. If set to 0 items, a string "0" is passed. Therefore, we unset @@ -385,7 +387,9 @@ class SearchApiViewsQuery extends views_plugin_query { $view->execute_time = microtime(TRUE) - $start; // Trigger pager post_execute(). - $this->pager->post_execute($view->result); + if ($this->pager) { + $this->pager->post_execute($view->result); + } } catch (Exception $e) { $this->errors[] = $e->getMessage(); From db8a350b3cfb36d3958dfb352f700bc883d254d0 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 15 Jun 2016 11:39:14 +0200 Subject: [PATCH 215/278] Issue #2744189 by nikolabintev, drunken monkey: Fixed highlighting for single-word fields. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ac9959fd..9a154214 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2744189 by nikolabintev, drunken monkey: Fixed highlighting for single-word + fields. - #2744995 by John Cook, drunken monkey: Fixed search views without pager. - #2742053 by tunic: Fixed change notification on node access records change. - #2733447 by jsacksick: Fixed translatability of our Views taxonomy term diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 62aa1581..8accf4de 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -450,12 +450,12 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { * @param array $array * The array to flatten. * @param string $glue - * The separator to insert between individual array items. + * (optional) The separator to insert between individual array items. * * @return string * The glued string. */ - protected function flattenArrayValues(array $array, $glue = "\n\n") { + protected function flattenArrayValues(array $array, $glue = " \n\n ") { $ret = array(); foreach ($array as $item) { if (is_array($item)) { From 5890fa4e820b0dee4319caeefdeb437ea45f89f2 Mon Sep 17 00:00:00 2001 From: StefanPr Date: Wed, 15 Jun 2016 12:04:13 +0200 Subject: [PATCH 216/278] Issue #2724687 by StefanPr, drunken monkey: Fixed failed sanitization of NULL field values. --- CHANGELOG.txt | 2 ++ search_api.module | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9a154214..41e0b00d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2724687 by StefanPr, drunken monkey: Fixed failed sanitization of NULL field + values. - #2744189 by nikolabintev, drunken monkey: Fixed highlighting for single-word fields. - #2744995 by John Cook, drunken monkey: Fixed search views without pager. diff --git a/search_api.module b/search_api.module index 072b0a34..0890fca4 100644 --- a/search_api.module +++ b/search_api.module @@ -2931,6 +2931,9 @@ function search_api_get_sanitized_field_values(array $values) { * The sanitized field value(s). */ function search_api_sanitize_field_value($field_value, $sanitize_callback = 'check_plain') { + if ($field_value === NULL) { + return $field_value; + } if (is_scalar($field_value)) { return call_user_func($sanitize_callback, $field_value); } From 6db63ee7ef96af7e026ef3710bc34f90c45c5ab6 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 5 Jul 2016 11:00:36 +0200 Subject: [PATCH 217/278] Adapted CHANGELOG.txt to 1.19 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 41e0b00d..b66670d1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.19 (2016-07-05): +----------------------------- - #2724687 by StefanPr, drunken monkey: Fixed failed sanitization of NULL field values. - #2744189 by nikolabintev, drunken monkey: Fixed highlighting for single-word From c3382e510e0e867077aab190cb56c7cc2026104d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 5 Jul 2016 11:00:36 +0200 Subject: [PATCH 218/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b66670d1..33331012 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.19 (2016-07-05): ----------------------------- - #2724687 by StefanPr, drunken monkey: Fixed failed sanitization of NULL field From 93843599e140a850f70432bddd1571197a91070c Mon Sep 17 00:00:00 2001 From: johnnyvdlaar Date: Wed, 6 Jul 2016 10:50:59 +0200 Subject: [PATCH 219/278] Issue #2753441 by Johnny vd Laar: Fixed translated field names in language-independent cache. --- CHANGELOG.txt | 2 ++ includes/index_entity.inc | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 33331012..be494c44 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2753441 by Johnny vd Laar: Fixed translated field names in + language-independent cache. Search API 1.19 (2016-07-05): ----------------------------- diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 7a4234a5..95848f12 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -764,12 +764,14 @@ class SearchApiIndex extends Entity { * "additional fields" key. */ public function getFields($only_indexed = TRUE, $get_additional = FALSE) { + global $language; + $only_indexed = $only_indexed ? 1 : 0; $get_additional = $get_additional ? 1 : 0; // First, try the static cache and the persistent cache bin. if (empty($this->fields[$only_indexed][$get_additional])) { - $cid = $this->getCacheId() . "-$only_indexed-$get_additional"; + $cid = $this->getCacheId() . "-$only_indexed-$get_additional-{$language->language}"; $cache = cache_get($cid); if ($cache) { $this->fields[$only_indexed][$get_additional] = $cache->data; From 893bd592260e629105f06256a276d2d4f6cd621b Mon Sep 17 00:00:00 2001 From: morningtime Date: Tue, 19 Jul 2016 12:58:23 +0200 Subject: [PATCH 220/278] Issue #1818572 by morningtime, drunken monkey, lodey, guillaumev: Added pretty paths support to the Views facets block. --- CHANGELOG.txt | 2 ++ .../includes/display_facet_block.inc | 25 +++++++++++++++++++ .../includes/handler_filter_entity.inc | 11 -------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index be494c44..cab37837 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1818572 by morningtime, drunken monkey, lodey, guillaumev: Added pretty + paths support to the Views facets block. - #2753441 by Johnny vd Laar: Fixed translated field names in language-independent cache. diff --git a/contrib/search_api_views/includes/display_facet_block.inc b/contrib/search_api_views/includes/display_facet_block.inc index 35ad14f5..e3e29191 100644 --- a/contrib/search_api_views/includes/display_facet_block.inc +++ b/contrib/search_api_views/includes/display_facet_block.inc @@ -247,6 +247,31 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { ), ); + // Override the $variables['#path'] if facetapi_pretty_paths is enabled. + if (module_exists('facetapi_pretty_paths')) { + // Get the appropriate facet adapter. + $adapter = facetapi_adapter_load('search_api@' . $index->machine_name); + + // Get the URL processor and check if it uses pretty paths. + $urlProcessor = $adapter->getUrlProcessor(); + if ($urlProcessor instanceof FacetapiUrlProcessorPrettyPaths) { + // Retrieve the pretty path alias from the URL processor. + $facet = facetapi_facet_load($facet_field, 'search_api@' . $index->machine_name); + $values = array(trim($term['filter'], '"')); + + // Get the pretty path for the facet and remove the current search's + // base path from it. + $base_path_current = $urlProcessor->getBasePath(); + $pretty_path = $urlProcessor->getFacetPath($facet, $values, FALSE); + $pretty_path = str_replace($base_path_current, '', $pretty_path); + + // Set the new, pretty path for the facet and remove the "f" query + // parameter. + $variables['path'] = $variables['path'] . $pretty_path; + unset($variables['options']['query']['f']); + } + } + // Themes the link, adds row to facets. $facets[] = array( 'class' => array('leaf'), diff --git a/contrib/search_api_views/includes/handler_filter_entity.inc b/contrib/search_api_views/includes/handler_filter_entity.inc index 8d2fe938..ce5c753d 100644 --- a/contrib/search_api_views/includes/handler_filter_entity.inc +++ b/contrib/search_api_views/includes/handler_filter_entity.inc @@ -67,17 +67,6 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi return $operators; } - /** - * {@inheritdoc} - */ - public function option_definition() { - $options = parent::option_definition(); - - $options['expose']['contains']['multiple']['default'] = TRUE; - - return $options; - } - /** * {@inheritdoc} */ From 20fc8aabb2063197b74e808fcc544396a764774e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Jul 2016 17:47:18 +0200 Subject: [PATCH 221/278] Issue #2731103 by drunken monkey: Fixed the default value for the taxonomy term filter "multiple" setting. --- CHANGELOG.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cab37837..14d00784 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2731103 by drunken monkey: Fixed the default value for the taxonomy term + filter "multiple" setting. - #1818572 by morningtime, drunken monkey, lodey, guillaumev: Added pretty paths support to the Views facets block. - #2753441 by Johnny vd Laar: Fixed translated field names in From a093d897e02671ab796e4039a3c1eb129a0d7f8e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Jul 2016 17:48:03 +0200 Subject: [PATCH 222/278] Adapted CHANGELOG.txt to 1.20 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 14d00784..5e5ec734 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.20 (2016-07-21): +----------------------------- - #2731103 by drunken monkey: Fixed the default value for the taxonomy term filter "multiple" setting. - #1818572 by morningtime, drunken monkey, lodey, guillaumev: Added pretty From 08011544890b2199d87b05e44aec139699a387c1 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 21 Jul 2016 17:48:03 +0200 Subject: [PATCH 223/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5e5ec734..84e5b942 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.20 (2016-07-21): ----------------------------- - #2731103 by drunken monkey: Fixed the default value for the taxonomy term From 5cf51a08b1cb45b40bc62e469f02086482b25f62 Mon Sep 17 00:00:00 2001 From: mfernea Date: Thu, 21 Jul 2016 18:39:26 +0200 Subject: [PATCH 224/278] Issue #2769877 by mfernea: Fixed database exception when filtering for anonymous user. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/handler_filter_user.inc | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 84e5b942..b8f8c6cf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2769877 by mfernea: Fixed database exception when filtering for anonymous + user. Search API 1.20 (2016-07-21): ----------------------------- diff --git a/contrib/search_api_views/includes/handler_filter_user.inc b/contrib/search_api_views/includes/handler_filter_user.inc index a2ef3ea2..6255274f 100644 --- a/contrib/search_api_views/includes/handler_filter_user.inc +++ b/contrib/search_api_views/includes/handler_filter_user.inc @@ -29,8 +29,10 @@ class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity protected function ids_to_strings(array $ids) { $names = array(); $args[':uids'] = array_filter($ids); - $result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args); - $result = $result->fetchAllKeyed(); + if ($args[':uids']) { + $result = db_query('SELECT uid, name FROM {users} u WHERE uid IN (:uids)', $args); + $result = $result->fetchAllKeyed(); + } foreach ($ids as $uid) { if (!$uid) { $names[] = variable_get('anonymous', t('Anonymous')); From ef5fc555e11b7ed0f6a006f0d904cabcd75ead30 Mon Sep 17 00:00:00 2001 From: Plazik Date: Mon, 1 Aug 2016 14:29:34 +0200 Subject: [PATCH 225/278] Issue #2769021 by Plazik, drunken monkey: Added the generated Search API query to the Views preview. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 3 +++ includes/query.inc | 25 ++++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b8f8c6cf..66e9c55c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2769021 by Plazik, drunken monkey: Added the generated Search API query to + the Views preview. - #2769877 by mfernea: Fixed database exception when filtering for anonymous user. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index bc9af4de..4394c5f3 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -310,6 +310,9 @@ class SearchApiViewsQuery extends views_plugin_query { if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) { $this->query->setOption('search_api_base_path', $this->view->override_path); } + + // Save query information for Views UI. + $view->build_info['query'] = (string) $this->query; } /** diff --git a/includes/query.inc b/includes/query.inc index debed66a..086ab424 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -856,10 +856,33 @@ class SearchApiQuery implements SearchApiQueryInterface { } $ret .= 'Sorting: ' . implode(', ', $sort) . "\n"; } - $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n"; + $options = $this->sanitizeOptions($this->options); + $options = str_replace("\n", "\n ", var_export($options, TRUE)); + $ret .= 'Options: ' . $options . "\n"; return $ret; } + /** + * Sanitizes an array of options in a way that plays nice with var_export(). + * + * @param array $options + * An array of options. + * + * @return array + * The sanitized options. + */ + protected function sanitizeOptions(array $options) { + foreach ($options as $key => $value) { + if (is_object($value)) { + $options[$key] = 'object (' . get_class($value) . ')'; + } + elseif (is_array($value)) { + $options[$key] = $this->sanitizeOptions($value); + } + } + return $options; + } + } /** From 4c53be2e0aacf307f4b5ce0d07a2159c5df928ac Mon Sep 17 00:00:00 2001 From: relaxnow Date: Mon, 22 Aug 2016 12:35:32 +0200 Subject: [PATCH 226/278] Issue #2649412 by relaxnow, GoZ: Added support for minimum granularity to date facets. --- CHANGELOG.txt | 2 ++ .../plugins/facetapi/adapter.inc | 14 +++++++++++++ .../plugins/facetapi/query_type_date.inc | 21 +++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 66e9c55c..195c99a0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2649412 by relaxnow, GoZ: Added support for minimum granularity to date + facets. - #2769021 by Plazik, drunken monkey: Added the generated Search API query to the Views preview. - #2769877 by mfernea: Fixed database exception when filtering for anonymous diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index 6251e32b..417b57b6 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -281,6 +281,20 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { // Date facets don't support the "OR" operator (for now). $form['global']['operator']['#access'] = FALSE; + + $default_value = FACETAPI_DATE_YEAR; + if (isset($options['date_granularity_min'])) { + $default_value = $options['date_granularity_min']; + } + $form['global']['date_granularity_min'] = array( + '#type' => 'select', + '#title' => t('Minimum granularity'), + '#description' => t('Determine the minimum drill-down level to start at'), + '#prefix' => '
    ', + '#suffix' => '
    ', + '#options' => $granularity_options, + '#default_value' => $default_value, + ); } // Add an "Exclude" option for terms. diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 6741cd81..565493dc 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -245,9 +245,19 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue } } - // Get the finest level of detail we're allowed to drill down to. $settings = $facet->getSettings()->settings; - $max_granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE; + + // Get the finest level of detail we're allowed to drill down to. + $max_granularity = FACETAPI_DATE_MINUTE; + if (isset($settings['date_granularity'])) { + $max_granularity = $settings['date_granularity']; + } + + // Get the coarsest level of detail we're allowed to start at. + $min_granularity = FACETAPI_DATE_YEAR; + if (isset($settings['date_granularity_min'])) { + $min_granularity = $settings['date_granularity_min']; + } // Gets active facets, starts building hierarchy. $parent = $granularity = NULL; @@ -301,11 +311,14 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue FACETAPI_DATE_MINUTE => 2, FACETAPI_DATE_SECOND => 1, ); - // Gets gap numbers for both the gap and minimum gap, checks if the gap - // is within the limit set by the $granularity parameter. + // Gets gap numbers for both the gap, minimum and maximum gap, checks if + // the gap is within the limit set by the $granularity parameters. if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) { $granularity = $max_granularity; } + if ($gap_numbers[$granularity] > $gap_numbers[$min_granularity]) { + $granularity = $min_granularity; + } } else { $granularity = $max_granularity; From 319370958cf5a8cb55b1afd428b88b5c592836e3 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 23 Aug 2016 11:20:42 +0200 Subject: [PATCH 227/278] Fixed literal use of 'item_id' in datasource instead of property. --- includes/datasource.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/datasource.inc b/includes/datasource.inc index eec9a79c..d7ceaa77 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -715,7 +715,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } $this->checkIndex($index); $select = db_select($this->table, 't'); - $select->addField('t', 'item_id'); + $select->addField('t', $this->itemIdColumn); $select->condition($this->indexIdColumn, $index->id); $select->condition($this->changedColumn, 0, '>'); $select->orderBy($this->changedColumn, 'ASC'); From 7b661ee3211a89ed5dbcae665fb703fcc5fc0fd0 Mon Sep 17 00:00:00 2001 From: markfullmer Date: Wed, 19 Oct 2016 12:18:51 +0200 Subject: [PATCH 228/278] Issue #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor. --- CHANGELOG.txt | 1 + README.txt | 4 + includes/processor_stemmer.inc | 732 +++++++++++++++++++++++++++++++++ search_api.info | 1 + search_api.module | 8 +- 5 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 includes/processor_stemmer.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 195c99a0..276464c3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor. - #2649412 by relaxnow, GoZ: Added support for minimum granularity to date facets. - #2769021 by Plazik, drunken monkey: Added the generated Search API query to diff --git a/README.txt b/README.txt index 2e2f581f..3943ceec 100644 --- a/README.txt +++ b/README.txt @@ -385,6 +385,10 @@ Included components Enables the admin to specify a stopwords file, the words contained in which will be filtered out of the text data indexed. This can be used to exclude too common words from indexing, for servers not supporting this natively. + * Stem words + Uses the PorterStemmer method to reduce words to stems. A search for + "garden" will return results for "gardening" and "garden," as will a search + for "gardening." - Additional modules diff --git a/includes/processor_stemmer.inc b/includes/processor_stemmer.inc new file mode 100644 index 00000000..a6c05489 --- /dev/null +++ b/includes/processor_stemmer.inc @@ -0,0 +1,732 @@ + url('https://github.com/markfullmer/porter2'), + '!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'), + ); + $form += array( + 'help' => array( + '#markup' => '

    ' . t('Optionally, provide an exclusion list to override the stemmer algorithm. Read about the algorithm and exclusions.', $args) . '

    ', + ), + 'exceptions' => array( + '#type' => 'textarea', + '#title' => t('Exceptions'), + '#description' => t('Enter exceptions in the form of WORD=STEM, where "WORD" is the term entered and "STEM" is the resulting stem. List each exception on a separate line.'), + '#default_value' => "texan=texa", + ), + ); + + if (!empty($this->options['exceptions'])) { + $form['exceptions']['#default_value'] = $this->options['exceptions']; + } + return $form; + } + + /** + * {@inheritdoc} + */ + protected function process(&$value) { + // Load custom exceptions. + $exceptions = $this->getExceptions(); + + $words = preg_split('/[^\p{L}\p{N}]+/u', $value, -1 , PREG_SPLIT_DELIM_CAPTURE); + $stemmed = array(); + foreach ($words as $i => $word) { + if ($i % 2 == 0 && strlen($word)) { + if (!isset($this->stems[$word])) { + $stem = new SearchApiPorter2($word, $exceptions); + $this->stems[$word] = $stem->stem(); + } + $stemmed[] = $this->stems[$word]; + } + else { + $stemmed[] = $word; + } + } + $value = implode('', $stemmed); + } + + /** + * Retrieves the processor's configured exceptions. + * + * @return string[] + * An associative array of exceptions, with words as keys and stems as their + * replacements. + */ + protected function getExceptions() { + if (!empty($this->options['exceptions'])) { + $exceptions = parse_ini_string($this->options['exceptions'], TRUE); + return is_array($exceptions) ? $exceptions : array(); + } + return array(); + } + +} + +/** + * Implements the Porter2 stemming algorithm. + * + * @see https://github.com/markfullmer/porter2 + */ +class SearchApiPorter2 { + + /** + * The word being stemmed. + * + * @var string + */ + protected $word; + + /** + * The R1 of the word. + * + * @var int + * + * @see http://snowball.tartarus.org/texts/r1r2.html. + */ + protected $r1; + + /** + * The R2 of the word. + * + * @var int + * + * @see http://snowball.tartarus.org/texts/r1r2.html. + */ + protected $r2; + + /** + * List of exceptions to be used. + * + * @var string[] + */ + protected $exceptions = array(); + + /** + * Constructs a SearchApiPorter2 object. + * + * @param string $word + * The word to stem. + * @param string[] $custom_exceptions + * (optional) A custom list of exceptions. + */ + public function __construct($word, $custom_exceptions = array()) { + $this->word = $word; + $this->exceptions = $custom_exceptions + array( + 'skis' => 'ski', + 'skies' => 'sky', + 'dying' => 'die', + 'lying' => 'lie', + 'tying' => 'tie', + 'idly' => 'idl', + 'gently' => 'gentl', + 'ugly' => 'ugli', + 'early' => 'earli', + 'only' => 'onli', + 'singly' => 'singl', + 'sky' => 'sky', + 'news' => 'news', + 'howe' => 'howe', + 'atlas' => 'atlas', + 'cosmos' => 'cosmos', + 'bias' => 'bias', + 'andes' => 'andes', + ); + + // Set initial y, or y after a vowel, to Y. + $inc = 0; + while ($inc <= $this->length()) { + if (substr($this->word, $inc, 1) === 'y' && ($inc == 0 || $this->isVowel($inc - 1))) { + $this->word = substr_replace($this->word, 'Y', $inc, 1); + + } + $inc++; + } + // Establish the regions R1 and R2. See function R(). + $this->r1 = $this->R(1); + $this->r2 = $this->R(2); + } + + /** + * Computes the stem of the word. + * + * @return string + * The word's stem. + */ + public function stem() { + // Ignore exceptions & words that are two letters or less. + if ($this->exceptions() || $this->length() <= 2) { + return strtolower($this->word); + } + else { + $this->step0(); + $this->step1a(); + $this->step1b(); + $this->step1c(); + $this->step2(); + $this->step3(); + $this->step4(); + $this->step5(); + } + return strtolower($this->word); + } + + /** + * Determines whether the word is contained in our list of exceptions. + * + * If so, the $word property is changed to the stem listed in the exceptions. + * + * @return bool + * TRUE if the word was an exception, FALSE otherwise. + */ + protected function exceptions() { + if (isset($this->exceptions[$this->word])) { + $this->word = $this->exceptions[$this->word]; + return TRUE; + } + return FALSE; + } + + /** + * Searches for the longest among the "s" suffixes and removes it. + * + * Implements step 0 of the Porter2 algorithm. + */ + protected function step0() { + $found = FALSE; + $checks = array("'s'", "'s", "'"); + foreach ($checks as $check) { + if (!$found && $this->hasEnding($check)) { + $this->removeEnding($check); + $found = TRUE; + } + } + } + + /** + * Handles various suffixes, of which the longest is replaced. + * + * Implements step 1a of the Porter2 algorithm. + */ + protected function step1a() { + $found = FALSE; + if ($this->hasEnding('sses')) { + $this->removeEnding('sses'); + $this->addEnding('ss'); + $found = TRUE; + } + $checks = array('ied', 'ies'); + foreach ($checks as $check) { + if (!$found && $this->hasEnding($check)) { + $length = $this->length(); + $this->removeEnding($check); + if ($length > 4) { + $this->addEnding('i'); + } + else { + $this->addEnding('ie'); + } + $found = TRUE; + } + } + if ($this->hasEnding('us') || $this->hasEnding('ss')) { + $found = TRUE; + } + // Delete if preceding word part has a vowel not immediately before the s. + if (!$found && $this->hasEnding('s') && $this->containsVowel(substr($this->word, 0, -2))) { + $this->removeEnding('s'); + } + } + + /** + * Handles various suffixes, of which the longest is replaced. + * + * Implements step 1b of the Porter2 algorithm. + */ + protected function step1b() { + $exceptions = array( + 'inning', + 'outing', + 'canning', + 'herring', + 'earring', + 'proceed', + 'exceed', + 'succeed', + ); + if (in_array($this->word, $exceptions)) { + return; + } + $checks = array('eedly', 'eed'); + foreach ($checks as $check) { + if ($this->hasEnding($check)) { + if ($this->r1 !== $this->length()) { + $this->removeEnding($check); + $this->addEnding('ee'); + } + return; + } + } + $checks = array('ingly', 'edly', 'ing', 'ed'); + $second_endings = array('at', 'bl', 'iz'); + foreach ($checks as $check) { + // If the ending is present and the previous part contains a vowel. + if ($this->hasEnding($check) && $this->containsVowel(substr($this->word, 0, -strlen($check)))) { + $this->removeEnding($check); + foreach ($second_endings as $ending) { + if ($this->hasEnding($ending)) { + $this->addEnding('e'); + return; + } + } + // If the word ends with a double, remove the last letter. + $found = $this->removeDoubles(); + // If the word is short, add e (so hop -> hope). + if (!$found && ($this->isShort())) { + $this->addEnding('e'); + } + return; + } + } + } + + /** + * Replaces suffix y or Y with i if after non-vowel not @ word begin. + * + * Implements step 1c of the Porter2 algorithm. + */ + protected function step1c() { + if (($this->hasEnding('y') || $this->hasEnding('Y')) && $this->length() > 2 && !($this->isVowel($this->length() - 2))) { + $this->removeEnding('y'); + $this->addEnding('i'); + } + } + + /** + * Implements step 2 of the Porter2 algorithm. + */ + protected function step2() { + $checks = array( + "ization" => "ize", + "iveness" => "ive", + "fulness" => "ful", + "ational" => "ate", + "ousness" => "ous", + "biliti" => "ble", + "tional" => "tion", + "lessli" => "less", + "fulli" => "ful", + "entli" => "ent", + "ation" => "ate", + "aliti" => "al", + "iviti" => "ive", + "ousli" => "ous", + "alism" => "al", + "abli" => "able", + "anci" => "ance", + "alli" => "al", + "izer" => "ize", + "enci" => "ence", + "ator" => "ate", + "bli" => "ble", + "ogi" => "og", + ); + foreach ($checks as $find => $replace) { + if ($this->hasEnding($find)) { + if ($this->inR1($find)) { + $this->removeEnding($find); + $this->addEnding($replace); + } + return; + } + } + if ($this->hasEnding('li')) { + if ($this->length() > 4 && $this->validLi($this->charAt(-3))) { + $this->removeEnding('li'); + } + } + } + + /** + * Implements step 3 of the Porter2 algorithm. + */ + protected function step3() { + $checks = array( + 'ational' => 'ate', + 'tional' => 'tion', + 'alize' => 'al', + 'icate' => 'ic', + 'iciti' => 'ic', + 'ical' => 'ic', + 'ness' => '', + 'ful' => '', + ); + foreach ($checks as $find => $replace) { + if ($this->hasEnding($find)) { + if ($this->inR1($find)) { + $this->removeEnding($find); + $this->addEnding($replace); + } + return; + } + } + if ($this->hasEnding('ative')) { + if ($this->inR2('ative')) { + $this->removeEnding('ative'); + } + } + } + + /** + * Implements step 4 of the Porter2 algorithm. + */ + protected function step4() { + $checks = array( + 'ement', + 'ment', + 'ance', + 'ence', + 'able', + 'ible', + 'ant', + 'ent', + 'ion', + 'ism', + 'ate', + 'iti', + 'ous', + 'ive', + 'ize', + 'al', + 'er', + 'ic', + ); + foreach ($checks as $check) { + // Among the suffixes, if found and in R2, delete. + if ($this->hasEnding($check)) { + if ($this->inR2($check)) { + if ($check !== 'ion' || in_array($this->charAt(-4), array('s', 't'))) { + $this->removeEnding($check); + } + } + return; + } + } + } + + /** + * Implements step 5 of the Porter2 algorithm. + */ + protected function step5() { + if ($this->hasEnding('e')) { + // Delete if in R2, or in R1 and not preceded by a short syllable. + if ($this->inR2('e') || ($this->inR1('e') && !$this->isShortSyllable($this->length() - 3))) { + $this->removeEnding('e'); + } + return; + } + if ($this->hasEnding('l')) { + // Delete if in R2 and preceded by l. + if ($this->inR2('l') && $this->charAt(-2) == 'l') { + $this->removeEnding('l'); + } + } + } + + /** + * Removes certain double consonants from the word's end. + * + * @return bool + * TRUE if a match was found and removed, FALSE otherwise. + */ + protected function removeDoubles() { + $found = FALSE; + $doubles = array('bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt'); + foreach ($doubles as $double) { + if (substr($this->word, -2) == $double) { + $this->word = substr($this->word, 0, -1); + $found = TRUE; + break; + } + } + return $found; + } + + /** + * Checks whether a character is a vowel. + * + * @param int $position + * The character's position. + * @param string|null $word + * (optional) The word in which to check. Defaults to $this->word. + * @param string[] $additional + * (optional) Additional characters that should count as vowels. + * + * @return bool + * TRUE if the character is a vowel, FALSE otherwise. + */ + protected function isVowel($position, $word = NULL, $additional = array()) { + if ($word === NULL) { + $word = $this->word; + } + $vowels = array_merge(array('a', 'e', 'i', 'o', 'u', 'y'), $additional); + return in_array($this->charAt($position, $word), $vowels); + } + + /** + * Retrieves the character at the given position. + * + * @param int $position + * The 0-based index of the character. If a negative number is given, the + * position is counted from the end of the string. + * @param string|null $word + * (optional) The word from which to retrieve the character. Defaults to + * $this->word. + * + * @return string + * The character at the given position, or an empty string if the given + * position was illegal. + */ + protected function charAt($position, $word = NULL) { + if ($word === NULL) { + $word = $this->word; + } + $length = strlen($word); + if (abs($position) >= $length) { + return ''; + } + if ($position < 0) { + $position += $length; + } + return $word[$position]; + } + + /** + * Determines whether the word ends in a "vowel-consonant" suffix. + * + * Unless the word is only two characters long, it also checks that the + * third-last character is neither "w", "x" nor "Y". + * + * @param int|null $position + * (optional) If given, do not check the end of the word, but the character + * at the given position, and the next one. + * + * @return bool + * TRUE if the word has the described suffix, FALSE otherwise. + */ + protected function isShortSyllable($position = NULL) { + if ($position === NULL) { + $position = $this->length() - 2; + } + // A vowel at the beginning of the word followed by a non-vowel. + if ($position === 0) { + return $this->isVowel(0) && !$this->isVowel(1); + } + // Vowel followed by non-vowel other than w, x, Y and preceded by + // non-vowel. + $additional = array('w', 'x', 'Y'); + return !$this->isVowel($position - 1) && $this->isVowel($position) && !$this->isVowel($position + 1, NULL, $additional); + } + + /** + * Determines whether the word is short. + * + * A word is called short if it ends in a short syllable and if R1 is null. + * + * @return bool + * TRUE if the word is short, FALSE otherwise. + */ + protected function isShort() { + return $this->isShortSyllable() && $this->r1 == $this->length(); + } + + /** + * Determines the start of a certain "R" region. + * + * R is a region after the first non-vowel following a vowel, or end of word. + * + * @param int $type + * (optional) 1 or 2. If 2, then calculate the R after the R1. + * + * @return int + * The R position. + */ + protected function R($type = 1) { + $inc = 1; + if ($type === 2) { + $inc = $this->r1; + } + elseif ($this->length() > 5) { + $prefix_5 = substr($this->word, 0, 5); + if ($prefix_5 === 'gener' || $prefix_5 === 'arsen') { + return 5; + } + if ($this->length() > 6 && substr($this->word, 0, 6) === 'commun') { + return 6; + } + } + + while ($inc <= $this->length()) { + if (!$this->isVowel($inc) && $this->isVowel($inc - 1)) { + $position = $inc; + break; + } + $inc++; + } + if (!isset($position)) { + $position = $this->length(); + } + else { + // We add one, as this is the position AFTER the first non-vowel. + $position++; + } + return $position; + } + + /** + * Checks whether the given string is contained in R1. + * + * @param string $string + * The string. + * + * @return bool + * TRUE if the string is in R1, FALSE otherwise. + */ + protected function inR1($string) { + $r1 = substr($this->word, $this->r1); + return strpos($r1, $string) !== FALSE; + } + + /** + * Checks whether the given string is contained in R2. + * + * @param string $string + * The string. + * + * @return bool + * TRUE if the string is in R2, FALSE otherwise. + */ + protected function inR2($string) { + $r2 = substr($this->word, $this->r2); + return strpos($r2, $string) !== FALSE; + } + + /** + * Determines the string length of the current word. + * + * @return int + * The string length of the current word. + */ + protected function length() { + return strlen($this->word); + } + + /** + * Checks whether the word ends with the given string. + * + * @param string $string + * The string. + * + * @return bool + * TRUE if the word ends with the given string, FALSE otherwise. + */ + protected function hasEnding($string) { + $length = strlen($string); + if ($length > $this->length()) { + return FALSE; + } + return (substr_compare($this->word, $string, -1 * $length, $length) === 0); + } + + /** + * Appends a given string to the current word. + * + * @param string $string + * The ending to append. + */ + protected function addEnding($string) { + $this->word = $this->word . $string; + } + + /** + * Removes a given string from the end of the current word. + * + * Does not check whether the ending is actually there. + * + * @param string $string + * The ending to remove. + */ + protected function removeEnding($string) { + $this->word = substr($this->word, 0, -strlen($string)); + } + + /** + * Checks whether the given string contains a vowel. + * + * @param string $string + * The string to check. + * + * @return bool + * TRUE if the string contains a vowel, FALSE otherwise. + */ + protected function containsVowel($string) { + $inc = 0; + $return = FALSE; + while ($inc < strlen($string)) { + if ($this->isVowel($inc, $string)) { + $return = TRUE; + break; + } + $inc++; + } + return $return; + } + + /** + * Checks whether the given string is a valid -li prefix. + * + * @param string $string + * The string to check. + * + * @return bool + * TRUE if the given string is a valid -li prefix, FALSE otherwise. + */ + protected function validLi($string) { + return in_array($string, array( + 'c', + 'd', + 'e', + 'g', + 'h', + 'k', + 'm', + 'n', + 'r', + 't', + )); + } + +} diff --git a/search_api.info b/search_api.info index 06cb0242..081fd4e6 100644 --- a/search_api.info +++ b/search_api.info @@ -27,6 +27,7 @@ files[] = includes/processor.inc files[] = includes/processor_highlight.inc files[] = includes/processor_html_filter.inc files[] = includes/processor_ignore_case.inc +files[] = includes/processor_stemmer.inc files[] = includes/processor_stopwords.inc files[] = includes/processor_tokenizer.inc files[] = includes/processor_transliteration.inc diff --git a/search_api.module b/search_api.module index 0890fca4..155f6db5 100644 --- a/search_api.module +++ b/search_api.module @@ -1152,11 +1152,17 @@ function search_api_search_api_processor_info() { 'class' => 'SearchApiStopWords', 'weight' => 30, ); + $processors['search_api_porter_stemmer'] = array( + 'name' => t('Stem words'), + 'description' => t('This processor reduces words to a stem (e.g., "talking" to "talk"). For best results, it should only be executed after tokenizing.'), + 'class' => 'SearchApiPorterStemmer', + 'weight' => 35, + ); $processors['search_api_highlighting'] = array( 'name' => t('Highlighting'), 'description' => t('Adds highlighting for search results.'), 'class' => 'SearchApiHighlight', - 'weight' => 35, + 'weight' => 40, ); return $processors; From 5447fe053abd765817341da940646ec721792d51 Mon Sep 17 00:00:00 2001 From: jelles Date: Wed, 19 Oct 2016 13:59:50 +0200 Subject: [PATCH 229/278] Issue #2358065 by Jelle_S, graper, drunken monkey: Added the option for highlighting of partial matches to the processor. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 45 ++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 276464c3..bfff62d7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2358065 by Jelle_S, graper, drunken monkey: Added the option for + highlighting of partial matches to the processor. - #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor. - #2649412 by relaxnow, GoZ: Added support for minimum granularity to date facets. diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 8accf4de..5b419491 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -51,6 +51,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { 'excerpt' => TRUE, 'excerpt_length' => 256, 'highlight' => 'always', + 'highlight_partial' => FALSE, 'exclude_fields' => array(), ); @@ -114,6 +115,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { '#default_value' => $this->options['highlight'], ); + $form['highlight_partial'] = array( + '#type' => 'checkbox', + '#title' => t('Highlight partial matches'), + '#description' => t('When enabled, matches in parts of words will be highlighted as well.'), + '#default_value' => $this->options['highlight_partial'], + ); + return $form; } @@ -322,9 +330,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { $ranges = array(); $included = array(); $length = 0; - $workkeys = $keys; - while ($length < $this->options['excerpt_length'] && count($workkeys)) { - foreach ($workkeys as $k => $key) { + $work_keys = $keys; + while ($length < $this->options['excerpt_length'] && $work_keys) { + foreach ($work_keys as $k => $key) { if ($length >= $this->options['excerpt_length']) { break; } @@ -336,8 +344,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { // Locate a keyword (position $p, always >0 because $text starts with a // space). $p = 0; - if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) { - $p = $match[0][1]; + if (empty($this->options['highlight_partial'])) { + $regex = '/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu'; + if (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) { + $p = $match[0][1]; + } + } + else { + $p = stripos($text, $key, $included[$key]); } // Now locate a space in front (position $q) and behind it (position $s), // leaving about 60 characters extra before and after for context. @@ -352,18 +366,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { $ranges[$q] = $p + $s; $length += $p + $s - $q; $included[$key] = $p + 1; - } - else { - unset($workkeys[$k]); + continue; } } - else { - unset($workkeys[$k]); - } - } - else { - unset($workkeys[$k]); } + // Unless we got a match above, we don't need to look for this key any + // more. + unset($work_keys[$k]); } } @@ -437,10 +446,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { } return implode('', $texts); } - $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); - $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); + // If "Highlight partial matches" is disabled, we only want to highlight + // matches that are complete words. Otherwise, we want all of them. + $boundary = empty($this->options['highlight_partial']) ? self::$boundary : ''; + $regex = '/' . $boundary . '(?:' . $keys . ')' . $boundary . '/iu'; + $replace = $this->options['prefix'] . '\0' . $this->options['suffix']; + $text = preg_replace($regex, $replace, ' ' . $text . ' '); return substr($text, 1, -1); } From 88c0d9b6532547f6a3d3b1e821c91499f4f41c37 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 26 Oct 2016 10:48:57 +0200 Subject: [PATCH 230/278] Issue #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately" functionality for unindexed items. --- CHANGELOG.txt | 2 ++ includes/datasource.inc | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bfff62d7..6d02c2a8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately" + functionality for unindexed items. - #2358065 by Jelle_S, graper, drunken monkey: Added the option for highlighting of partial matches to the processor. - #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor. diff --git a/includes/datasource.inc b/includes/datasource.inc index d7ceaa77..9661092c 100644 --- a/includes/datasource.inc +++ b/includes/datasource.inc @@ -626,8 +626,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou return NULL; } - $ret = array(); - + $indexes_by_id = array(); foreach ($indexes as $index) { $this->checkIndex($index); $update = db_update($this->table) @@ -639,12 +638,26 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou if ($item_ids !== FALSE) { $update->condition($this->itemIdColumn, $item_ids, 'IN'); } - if ($update->execute()) { - $ret[] = $index; - } + $update->execute(); + $indexes_by_id[$index->id] = $index; } - return $ret; + // Determine and return the indexes with any changed items. If $item_ids is + // FALSE, all items are marked as changed and, thus, all indexes will be + // affected (unless they don't have any items, but no real point in treating + // that special case). + if ($item_ids !== FALSE) { + $indexes_with_items = db_select($this->table, 't') + ->fields('t', array($this->indexIdColumn)) + ->distinct() + ->condition($this->indexIdColumn, array_keys($indexes_by_id), 'IN') + ->condition($this->itemIdColumn, $item_ids, 'IN') + ->execute() + ->fetchCol(); + return array_intersect_key($indexes_by_id, array_flip($indexes_with_items)); + } + + return NULL; } /** From 8afc105a82d60267656f35666b4422de06afa3fa Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 12 Nov 2016 10:32:22 +0100 Subject: [PATCH 231/278] Issue #2822145 by drunken monkey: Fixed problem with phrase search in Views fulltext filter. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_fulltext.inc | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6d02c2a8..cb16c2ad 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2822145 by drunken monkey: Fixed problem with phrase search in Views + fulltext filter. - #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately" functionality for unindexed items. - #2358065 by Jelle_S, graper, drunken monkey: Added the option for diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index de61c9bf..320adc36 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -119,7 +119,17 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex } $words = preg_split('/\s+/', $input); + $quoted = FALSE; foreach ($words as $i => $word) { + // Protect quoted strings. + if ($quoted && $word[strlen($word) - 1] === '"') { + $quoted = FALSE; + continue; + } + if ($quoted || $word[0] === '"') { + $quoted = TRUE; + continue; + } if (drupal_strlen($word) < $this->options['min_length']) { unset($words[$i]); } From c25e27bb567466f63fbd5270389003eb989e3a27 Mon Sep 17 00:00:00 2001 From: prince_zyxware Date: Sat, 12 Nov 2016 14:54:02 +0100 Subject: [PATCH 232/278] Issue #2822836 by prince_zyxware: Fixed some Drupal coding standards violations. --- CHANGELOG.txt | 1 + contrib/search_api_facetapi/plugins/facetapi/adapter.inc | 2 +- contrib/search_api_views/includes/display_facet_block.inc | 4 ++-- search_api.install | 2 +- search_api.rules.inc | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cb16c2ad..e1c627b3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2822836 by prince_zyxware: Fixed some Drupal coding standards violations. - #2822145 by drunken monkey: Fixed problem with phrase search in Views fulltext filter. - #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately" diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index 417b57b6..1b1da48f 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -298,7 +298,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { } // Add an "Exclude" option for terms. - if(!empty($facet['query types']) && in_array('term', $facet['query types'])) { + if (!empty($facet['query types']) && in_array('term', $facet['query types'])) { $form['global']['operator']['#weight'] = -2; unset($form['global']['operator']['#suffix']); $form['global']['exclude'] = array( diff --git a/contrib/search_api_views/includes/display_facet_block.inc b/contrib/search_api_views/includes/display_facet_block.inc index e3e29191..00e80c29 100644 --- a/contrib/search_api_views/includes/display_facet_block.inc +++ b/contrib/search_api_views/includes/display_facet_block.inc @@ -151,7 +151,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { } } - public function query(){ + public function query() { parent::query(); $facet_field = $this->get_option('facet_field'); @@ -291,7 +291,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { ); } - public function execute(){ + public function execute() { $info['content'] = $this->render(); $info['content']['more'] = $this->render_more_link(); $info['subject'] = filter_xss_admin($this->view->get_title()); diff --git a/search_api.install b/search_api.install index 34682d9c..5dc26895 100644 --- a/search_api.install +++ b/search_api.install @@ -362,7 +362,7 @@ function search_api_install() { ), ); search_api_index_insert($values); - drupal_set_message('The Search API module was installed. A new default node index was created.'); + drupal_set_message(t('The Search API module was installed. A new default node index was created.')); } /** diff --git a/search_api.rules.inc b/search_api.rules.inc index 37143f87..0725f4ce 100644 --- a/search_api.rules.inc +++ b/search_api.rules.inc @@ -10,7 +10,7 @@ * Implements hook_rules_action_info(). */ function search_api_rules_action_info() { - $items['search_api_index'] = array ( + $items['search_api_index'] = array( 'parameter' => array( 'entity' => array( 'type' => 'entity', From ee67f120d2b1f014f6424bfea47a5a433ed3dae2 Mon Sep 17 00:00:00 2001 From: git Date: Sat, 26 Nov 2016 12:08:02 +0100 Subject: [PATCH 233/278] Issue #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views when reverting an index. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/search_api_views.module | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e1c627b3..3f39b89a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views + when reverting an index. - #2822836 by prince_zyxware: Fixed some Drupal coding standards violations. - #2822145 by drunken monkey: Fixed problem with phrase search in Views fulltext filter. diff --git a/contrib/search_api_views/search_api_views.module b/contrib/search_api_views/search_api_views.module index 8a131c2a..62fcb944 100644 --- a/contrib/search_api_views/search_api_views.module +++ b/contrib/search_api_views/search_api_views.module @@ -45,7 +45,10 @@ function search_api_views_search_api_index_update(SearchApiIndex $index) { * Implements hook_search_api_index_delete(). */ function search_api_views_search_api_index_delete(SearchApiIndex $index) { - _search_api_views_index_unavailable($index); + // Only do this if this is a "real" deletion, no revert. + if (!$index->hasStatus(ENTITY_IN_CODE)) { + _search_api_views_index_unavailable($index); + } } /** From e3c2e1ff4952ae1f37762bb48add45ee8f8e194c Mon Sep 17 00:00:00 2001 From: jansete Date: Tue, 29 Nov 2016 16:40:50 +0100 Subject: [PATCH 234/278] Issue #2828380 by jansete: Fixed taxonomy term access tag in Views filter. --- CHANGELOG.txt | 1 + .../includes/handler_filter_taxonomy_term.inc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3f39b89a..f5c4f583 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2828380 by jansete: Fixed taxonomy term access tag in Views filter. - #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views when reverting an index. - #2822836 by prince_zyxware: Fixed some Drupal coding standards violations. diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index b6db4f69..f3317cfa 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -93,7 +93,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte $query->orderby('tv.name'); $query->orderby('td.weight'); $query->orderby('td.name'); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); if ($vocabulary) { $query->condition('tv.machine_name', $vocabulary->machine_name); } @@ -272,7 +272,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte if (!empty($this->definition['vocabulary'])) { $query->condition('tv.machine_name', $this->definition['vocabulary']); } - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); $result = $query->execute(); foreach ($result as $term) { unset($missing[strtolower($term->name)]); From a49ceae373673d9e5ffc4eb442036ce4496f4529 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 23 Dec 2016 16:06:41 +0100 Subject: [PATCH 235/278] Issue #2632880 by drunken monkey, donquixote: Added possibility to change indexed bundles on disabled indexes. --- CHANGELOG.txt | 2 ++ includes/datasource_entity.inc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f5c4f583..a9a533b9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2632880 by drunken monkey, donquixote: Added possibility to change indexed + bundles on disabled indexes. - #2828380 by jansete: Fixed taxonomy term access tag in Views filter. - #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views when reverting an index. diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc index 3ebbab24..bbca0aa4 100644 --- a/includes/datasource_entity.inc +++ b/includes/datasource_entity.inc @@ -278,10 +278,10 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon $form['bundles'] = array( '#type' => 'checkboxes', '#title' => t('Bundles'), - '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'), + '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for enabled indexes.'), '#options' => array_map('check_plain', $options), '#attributes' => array('class' => array('search-api-checkboxes-list')), - '#disabled' => !empty($form_state['index']), + '#disabled' => !empty($form_state['index']) && $form_state['index']->enabled, ); if (!empty($form_state['index']->options['datasource'])) { $form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']); From b33a62e1b585bb0df956c7ed005a2f30b67ffb27 Mon Sep 17 00:00:00 2001 From: git Date: Sun, 1 Jan 2017 14:38:44 +0100 Subject: [PATCH 236/278] Issue #2836687 by sarthak drupal: Fixed one doc comment typo. --- CHANGELOG.txt | 1 + includes/datasource_external.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a9a533b9..03650a91 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2836687 by sarthak drupal: Fixed one doc comment typo. - #2632880 by drunken monkey, donquixote: Added possibility to change indexed bundles on disabled indexes. - #2828380 by jansete: Fixed taxonomy term access tag in Views filter. diff --git a/includes/datasource_external.inc b/includes/datasource_external.inc index 1128f173..c3a9f1fc 100644 --- a/includes/datasource_external.inc +++ b/includes/datasource_external.inc @@ -49,7 +49,7 @@ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceC * loadable, specify a function here. * * @param array $ids - * The IDs of the items to laod. + * The IDs of the items to load. * * @return array * The loaded items, keyed by ID. From 4ac425e1a4ea694d1c965f0967c1bde2e792e64a Mon Sep 17 00:00:00 2001 From: dsnopek Date: Sun, 1 Jan 2017 15:03:51 +0100 Subject: [PATCH 237/278] Issue #2838075 by dsnopek: Fixed possible race condition in hook_system_info_alter(). --- CHANGELOG.txt | 2 ++ search_api.module | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 03650a91..0a25a0ec 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2838075 by dsnopek: Fixed possible race condition in + hook_system_info_alter(). - #2836687 by sarthak drupal: Fixed one doc comment typo. - #2632880 by drunken monkey, donquixote: Added possibility to change indexed bundles on disabled indexes. diff --git a/search_api.module b/search_api.module index 155f6db5..61385c5d 100644 --- a/search_api.module +++ b/search_api.module @@ -784,7 +784,7 @@ function search_api_features_export_alter(&$export) { * @see hook_search_api_item_type_info() */ function search_api_system_info_alter(&$info, $file, $type) { - if ($type != 'module' || $file->name == 'search_api') { + if ($type != 'module' || $file->name == 'search_api' || !module_exists($file->name)) { return; } // Check for defined item types. From 7b94a45dfde3c4e0f1ca864abceef38d4976dd1c Mon Sep 17 00:00:00 2001 From: KyleTaylored Date: Sat, 7 Jan 2017 16:27:12 +0100 Subject: [PATCH 238/278] Issue #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal error in facet adapter's getSearchKeys() method. --- CHANGELOG.txt | 2 ++ contrib/search_api_facetapi/plugins/facetapi/adapter.inc | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0a25a0ec..52446fc6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal + error in facet adapter's getSearchKeys() method. - #2838075 by dsnopek: Fixed possible race condition in hook_system_info_alter(). - #2836687 by sarthak drupal: Fixed one doc comment typo. diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index 1b1da48f..a5b5cdc0 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc @@ -192,6 +192,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { */ public function getSearchKeys() { $search = $this->getCurrentSearch(); + + // If the search is empty then there's no reason to continue. + if (!$search) { + return NULL; + } + $keys = $search[0]->getOriginalKeys(); if (is_array($keys)) { // This will happen nearly never when displaying the search keys to the From f03dc2843a8b8bb4b8c16381547baac31cd768e1 Mon Sep 17 00:00:00 2001 From: git Date: Sun, 22 Jan 2017 19:51:49 +0100 Subject: [PATCH 239/278] Issue #2840261 by alan-ps: Fixed usage of outdated hash functions. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/plugin_cache.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 52446fc6..1b5a76b5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2840261 by alan-ps: Fixed usage of outdated hash functions. - #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal error in facet adapter's getSearchKeys() method. - #2838075 by dsnopek: Fixed possible race condition in diff --git a/contrib/search_api_views/includes/plugin_cache.inc b/contrib/search_api_views/includes/plugin_cache.inc index 5e0dff34..c6bd41d4 100644 --- a/contrib/search_api_views/includes/plugin_cache.inc +++ b/contrib/search_api_views/includes/plugin_cache.inc @@ -103,7 +103,7 @@ class SearchApiViewsCache extends views_plugin_cache_time { $key_data['exposed_info'] = $_GET['exposed_info']; } } - $key = md5(serialize($key_data)); + $key = drupal_hash_base64(serialize($key_data)); return $key; } From 2dea835bdebadf2dc6f504e50a0dd4edd3fd3262 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Jan 2017 17:00:57 +0100 Subject: [PATCH 240/278] Issue #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets module. --- CHANGELOG.txt | 2 ++ .../search_api_facetapi.install | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1b5a76b5..1fd4a822 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets + module. - #2840261 by alan-ps: Fixed usage of outdated hash functions. - #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal error in facet adapter's getSearchKeys() method. diff --git a/contrib/search_api_facetapi/search_api_facetapi.install b/contrib/search_api_facetapi/search_api_facetapi.install index 5743e080..77c08026 100644 --- a/contrib/search_api_facetapi/search_api_facetapi.install +++ b/contrib/search_api_facetapi/search_api_facetapi.install @@ -22,12 +22,14 @@ function search_api_facetapi_install() { */ function search_api_facetapi_uninstall() { variable_del('search_api_facets_search_ids'); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE); - variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND); + // We have to use the literal values here, as the Facet API module could have + // already been disabled at this point. + variable_del('date_format_search_api_facetapi_YEAR'); + variable_del('date_format_search_api_facetapi_MONTH'); + variable_del('date_format_search_api_facetapi_DAY'); + variable_del('date_format_search_api_facetapi_HOUR'); + variable_del('date_format_search_api_facetapi_MINUTE'); + variable_del('date_format_search_api_facetapi_SECOND'); } /** From 7891a930ae4fc001a05c815c44bc9c2709390dc5 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 25 Jan 2017 17:02:56 +0100 Subject: [PATCH 241/278] Issue #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries. --- CHANGELOG.txt | 1 + includes/query.inc | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1fd4a822..d7eeafc2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries. - #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets module. - #2840261 by alan-ps: Fixed usage of outdated hash functions. diff --git a/includes/query.inc b/includes/query.inc index 086ab424..640dcd20 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -1071,6 +1071,10 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { * {@inheritdoc} */ public function &getTags() { + // Tags can sometimes be NULL for old serialized query filter objects. + if (!isset($this->tags)) { + $this->tags = array(); + } return $this->tags; } From 2c7aab9e3e3ceda29598b55ecf1ef89236aa58e3 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 6 Feb 2017 21:19:23 +0100 Subject: [PATCH 242/278] Issue #2844990 by drunken monkey: Made the "Role filter" data alteration available for multi-type indexes. --- CHANGELOG.txt | 2 ++ includes/callback.inc | 15 +++++++++++++++ includes/callback_bundle_filter.inc | 15 --------------- includes/callback_role_filter.inc | 19 ++++++++++++++++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d7eeafc2..c6ce6ab9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2844990 by drunken monkey: Made the "Role filter" data alteration available + for multi-type indexes. - #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries. - #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets module. diff --git a/includes/callback.inc b/includes/callback.inc index ea161fbd..617aee37 100644 --- a/includes/callback.inc +++ b/includes/callback.inc @@ -182,4 +182,19 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI return array(); } + /** + * Determines whether the given index contains multiple types of entities. + * + * @param SearchApiIndex|null $index + * (optional) The index to examine. Defaults to the index set for this + * plugin. + * + * @return bool + * TRUE if the index is a multi-entity index, FALSE otherwise. + */ + protected function isMultiEntityIndex(SearchApiIndex $index = NULL) { + $index = $index ? $index : $this->index; + return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController; + } + } diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index ab743d6c..cde8fe27 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -132,19 +132,4 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']); } - /** - * Determines whether the given index contains multiple types of entities. - * - * @param SearchApiIndex|null $index - * (optional) The index to examine. Defaults to the index set for this - * plugin. - * - * @return bool - * TRUE if the index is a multi-entity index, FALSE otherwise. - */ - protected function isMultiEntityIndex(SearchApiIndex $index = NULL) { - $index = $index ? $index : $this->index; - return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController; - } - } diff --git a/includes/callback_role_filter.inc b/includes/callback_role_filter.inc index ba126d1f..68b8d722 100644 --- a/includes/callback_role_filter.inc +++ b/includes/callback_role_filter.inc @@ -16,6 +16,9 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback { * This plugin only supports indexes containing users. */ public function supportsIndex(SearchApiIndex $index) { + if ($this->isMultiEntityIndex($index)) { + return in_array('user', $index->options['datasource']['types']); + } return $index->getEntityType() == 'user'; } @@ -23,10 +26,20 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback { * Implements SearchApiAlterCallbackInterface::alterItems(). */ public function alterItems(array &$items) { - $roles = $this->options['roles']; + $selected_roles = $this->options['roles']; $default = (bool) $this->options['default']; - foreach ($items as $id => $account) { - $role_match = (count(array_diff_key($account->roles, $roles)) !== count($account->roles)); + $multi_types = $this->isMultiEntityIndex($this->index); + foreach ($items as $id => $item) { + if ($multi_types) { + if ($item->item_type !== 'user') { + continue; + } + $item_roles = $item->user->roles; + } + else { + $item_roles = $item->roles; + } + $role_match = (count(array_diff_key($item_roles, $selected_roles)) !== count($item_roles)); if ($role_match === $default) { unset($items[$id]); } From fe202e779f49f5eaceb72114210584a3345bf6dd Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Mon, 6 Feb 2017 21:51:53 +0100 Subject: [PATCH 243/278] Issue #2842856 by drunken monkey: Fixed language filters for "Multiple types" indexes. --- CHANGELOG.txt | 2 ++ includes/datasource_multiple.inc | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c6ce6ab9..e498be22 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2842856 by drunken monkey: Fixed language filters for "Multiple types" + indexes. - #2844990 by drunken monkey: Made the "Role filter" data alteration available for multi-type indexes. - #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries. diff --git a/includes/datasource_multiple.inc b/includes/datasource_multiple.inc index 7e5d4de2..ea164963 100644 --- a/includes/datasource_multiple.inc +++ b/includes/datasource_multiple.inc @@ -44,6 +44,9 @@ class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataS $item->item_type = $type; $item->item_entity_id = $entity_id; $item->item_bundle = NULL; + // Add the item language so the "search_api_language" field will work + // correctly. + $item->language = isset($entity->language) ? $entity->language : NULL; try { list(, , $bundle) = entity_extract_ids($type, $entity); $item->item_bundle = $bundle ? "$type:$bundle" : NULL; From ebe00d1d37bb9e3f07134fee8963bfba2277b7bf Mon Sep 17 00:00:00 2001 From: git Date: Thu, 16 Feb 2017 18:12:10 +0100 Subject: [PATCH 244/278] Issue #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation type. --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e498be22..d91eea16 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation + type. - #2842856 by drunken monkey: Fixed language filters for "Multiple types" indexes. - #2844990 by drunken monkey: Made the "Role filter" data alteration available diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 9535be3c..069c4107 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -209,6 +209,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { return $a; } return drupal_substr($b, 0, 1); + case 'last': + return isset($b) ? $b : $a; case 'list': if (!isset($a)) { $a = array(); @@ -288,6 +290,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'min' => t('Minimum'), 'first' => t('First'), 'first_char' => t('First letter'), + 'last' => t('Last'), 'list' => t('List'), ); case 'type': @@ -299,6 +302,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'min' => 'integer', 'first' => 'string', 'first_char' => 'string', + 'last' => 'string', 'list' => 'list', ); case 'description': @@ -310,6 +314,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'), 'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'), 'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'), + 'last' => t('The Last aggregation will simply keep the last encountered field value.'), 'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'), ); } From c8a39a5b0f1616d3cb7a8f367ec2742b1c5da75b Mon Sep 17 00:00:00 2001 From: berdir Date: Wed, 22 Feb 2017 16:18:15 +0100 Subject: [PATCH 245/278] Issue #2780341 by Berdir: Fixed passing of custom ranges to date facets. --- CHANGELOG.txt | 1 + .../search_api_facetapi/plugins/facetapi/query_type_date.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d91eea16..981bad1e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2780341 by Berdir: Fixed passing of custom ranges to date facets. - #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation type. - #2842856 by drunken monkey: Fixed language filters for "Multiple types" diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 565493dc..6aeb8362 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc @@ -76,7 +76,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue */ protected function createRangeFilter($value) { // Ignore any filters passed directly from the server (range or missing). - if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ [^ ]+[])]$/', $value))) { + if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ TO [^ ]+[\])]$/', $value))) { return $value ? $value : NULL; } From 4489bb443a071ff602ad8d3ca737553a6f59512d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 22 Feb 2017 16:25:56 +0100 Subject: [PATCH 246/278] Issue #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration. --- CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 981bad1e..8ee5ec74 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration. - #2780341 by Berdir: Fixed passing of custom ranges to date facets. - #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation type. From 2b70f348a0eb6e8d24c6695126b17dfc0e8c0725 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Wed, 22 Feb 2017 16:58:16 +0100 Subject: [PATCH 247/278] Revert "Issue #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration." This reverts commit 4489bb443a071ff602ad8d3ca737553a6f59512d. --- CHANGELOG.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8ee5ec74..981bad1e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,5 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- -- #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration. - #2780341 by Berdir: Fixed passing of custom ranges to date facets. - #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation type. From bc9e81d9626d56d99aa4edc22a31bc3936ab7b8c Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 23 Feb 2017 11:01:52 +0100 Subject: [PATCH 248/278] Adapted CHANGELOG.txt to 1.21 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 981bad1e..0084fe0d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.21 (2017-02-23): +----------------------------- - #2780341 by Berdir: Fixed passing of custom ranges to date facets. - #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation type. From 37e9a66ffa1b0bf102587796230a78e4ff48e89d Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 23 Feb 2017 11:01:52 +0100 Subject: [PATCH 249/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0084fe0d..b4beaf1f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.21 (2017-02-23): ----------------------------- - #2780341 by Berdir: Fixed passing of custom ranges to date facets. From 4bd744f9d69a94052721dd55d7ef91a4c53aa884 Mon Sep 17 00:00:00 2001 From: dbjpanda Date: Thu, 20 Apr 2017 16:14:13 +0200 Subject: [PATCH 250/278] Issue #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.txt. --- CHANGELOG.txt | 1 + README.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b4beaf1f..5ba4ede0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.txt. Search API 1.21 (2017-02-23): ----------------------------- diff --git a/README.txt b/README.txt index 3943ceec..a6264e97 100644 --- a/README.txt +++ b/README.txt @@ -31,9 +31,9 @@ Terms as used in this module. Sphinx or any other professional or simple indexing mechanism. Takes care of the details of all operations, especially indexing or searching content. - Server: - One specific place for indexing data, using a set service class. Can - e.g. be some tables in a database, a connection to a Solr server or other - external services, etc. + One specific place for indexing data, using a specific service class. For + example this could be some tables in a database, a connection to a Solr server + or other external services, etc. - Index: A configuration object for indexing data of a specific type. What and how data is indexed is determined by its settings. Also keeps track of which items From 7eef4621006d80cd0d4f51f3fcc997c3e7c83c87 Mon Sep 17 00:00:00 2001 From: mparker17 Date: Tue, 25 Apr 2017 12:53:06 +0200 Subject: [PATCH 251/278] Issue #2855447 by mparker17, drunken monkey: Added "Separator" option for aggregated fields of type "Fulltext". --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 34 ++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5ba4ede0..6eecdc4f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2855447 by mparker17, drunken monkey: Added "Separator" option for + aggregated fields of type "Fulltext". - #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.txt. Search API 1.21 (2017-02-23): diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 069c4107..2e744f60 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -20,11 +20,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { */ protected $reductionType; + /** + * A separator to use when the aggregation type is 'fulltext'. + * + * Used to temporarily store a string separator when the aggregation type is + * "fulltext", for use in SearchApiAlterAddAggregation::reduce() with + * array_reduce(). + * + * @var string + */ + protected $fulltextReductionSeparator; + public function configurationForm() { $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; $fields = $this->index->getFields(FALSE); $field_options = array(); + $field_properties = array(); foreach ($fields as $name => $field) { $field_options[$name] = check_plain($field['name']); $field_properties[$name] = array( @@ -79,9 +91,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { '#required' => TRUE, ); $form['fields'][$name]['type_descriptions'] = $type_descriptions; + $type_selector = ':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]'; foreach (array_keys($types) as $type) { - $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type; + $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][$type_selector]['value'] = $type; } + $form['fields'][$name]['separator'] = array( + '#type' => 'textfield', + '#title' => t('Fulltext separator'), + '#description' => t('For aggregation type "Fulltext", set the text that should be used to separate the aggregated field values. Use "\t" for tabs and "\n" for newline characters.'), + '#default_value' => addcslashes(isset($field['separator']) ? $field['separator'] : "\n\n", "\0..\37\\"), + '#states' => array( + 'visible' => array( + $type_selector => array( + 'value' => 'fulltext', + ), + ), + ), + ); $form['fields'][$name]['fields'] = array_merge($field_properties, array( '#type' => 'checkboxes', '#title' => t('Contained fields'), @@ -125,11 +151,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { return; } foreach ($values['fields'] as $name => $field) { - $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields'])); unset($values['fields'][$name]['actions']); + $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields'])); if ($field['name'] && !$fields) { form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.')); } + $values['fields'][$name]['separator'] = stripcslashes($field['separator']); } } @@ -176,6 +203,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { $values = $this->flattenArray($values); $this->reductionType = $field['type']; + $this->fulltextReductionSeparator = isset($field['separator']) ? $field['separator'] : "\n\n"; $item->$name = array_reduce($values, array($this, 'reduce'), NULL); if ($field['type'] == 'count' && !$item->$name) { $item->$name = 0; @@ -192,7 +220,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { public function reduce($a, $b) { switch ($this->reductionType) { case 'fulltext': - return isset($a) ? $a . "\n\n" . $b : $b; + return isset($a) ? $a . $this->fulltextReductionSeparator . $b : $b; case 'sum': return $a + $b; case 'count': From c713dc7693f879bb94948d73bfd00d0a661c619f Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 25 Apr 2017 12:58:08 +0200 Subject: [PATCH 252/278] Issue #2860624 by drunken monkey: Fixed problem with empty words in Views fulltext filter. --- CHANGELOG.txt | 2 ++ .../search_api_views/includes/handler_filter_fulltext.inc | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6eecdc4f..8bdda995 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2860624 by drunken monkey: Fixed problem with empty words in Views fulltext + filter. - #2855447 by mparker17, drunken monkey: Added "Separator" option for aggregated fields of type "Fulltext". - #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.txt. diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc index 320adc36..dcb3e263 100644 --- a/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -121,6 +121,11 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex $words = preg_split('/\s+/', $input); $quoted = FALSE; foreach ($words as $i => $word) { + $word_length = drupal_strlen($word); + if (!$word_length) { + unset($words[$i]); + continue; + } // Protect quoted strings. if ($quoted && $word[strlen($word) - 1] === '"') { $quoted = FALSE; @@ -130,7 +135,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex $quoted = TRUE; continue; } - if (drupal_strlen($word) < $this->options['min_length']) { + if ($word_length < $this->options['min_length']) { unset($words[$i]); } } From d41c745ed5457da90402a749912b9475cfed6519 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 12 May 2017 13:13:04 +0200 Subject: [PATCH 253/278] Issue #2875793 by drunken monkey: Fixed buggy error handling in Views. --- CHANGELOG.txt | 1 + contrib/search_api_views/includes/query.inc | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8bdda995..6aec0626 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2875793 by drunken monkey: Fixed buggy error handling in Views. - #2860624 by drunken monkey: Fixed problem with empty words in Views fulltext filter. - #2855447 by mparker17, drunken monkey: Added "Separator" option for diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 4394c5f3..42dc3c22 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -135,7 +135,9 @@ class SearchApiViewsQuery extends views_plugin_query { * The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'. */ public function add_selector_orderby($selector, $order = 'ASC') { - $this->query->sort($selector, $order); + if (!$this->errors) { + $this->query->sort($selector, $order); + } } /** From a5ba346db1a670dff91b19b1dfbaaa33b1b66933 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 25 May 2017 16:15:20 +0200 Subject: [PATCH 254/278] Issue #2749963 by drunken monkey: Fixed "Index hierarchy" not having values numerically indexed. --- CHANGELOG.txt | 2 ++ includes/callback_add_hierarchy.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6aec0626..0c2092e5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2749963 by drunken monkey: Fixed "Index hierarchy" not having values + numerically indexed. - #2875793 by drunken monkey: Fixed buggy error handling in Views. - #2860624 by drunken monkey: Fixed problem with empty words in Views fulltext filter. diff --git a/includes/callback_add_hierarchy.inc b/includes/callback_add_hierarchy.inc index d69badb6..b9aada53 100644 --- a/includes/callback_add_hierarchy.inc +++ b/includes/callback_add_hierarchy.inc @@ -108,7 +108,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { $this->extractHierarchy($child, $prop, $values[$key]); } foreach ($values as $key => $value) { - $item->$key = $value; + $item->$key = array_values($value); } } } From df04cb1f322bc7264307d5ef8ec1077b8f62d69a Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 26 May 2017 17:04:31 +0200 Subject: [PATCH 255/278] Issue #2788593 by drunken monkey: Fixed error in Views query settings for specific setups. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/includes/query.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0c2092e5..39dc9e04 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2788593 by drunken monkey: Fixed error in Views query settings for specific + setups. - #2749963 by drunken monkey: Fixed "Index hierarchy" not having values numerically indexed. - #2875793 by drunken monkey: Fixed buggy error handling in Views. diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 42dc3c22..ae58b9a7 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -215,7 +215,7 @@ class SearchApiViewsQuery extends views_plugin_query { '#default_value' => $this->options['search_api_bypass_access'], ); - if ($this->index->getEntityType()) { + if ($this->index && $this->index->getEntityType()) { $form['entity_access'] = array( '#type' => 'checkbox', '#title' => t('Additional access checks on result entities'), From 41813a1270ec347ce1c53708d896f0f3428d5180 Mon Sep 17 00:00:00 2001 From: blacklabel_tom Date: Sun, 18 Jun 2017 11:59:42 +0200 Subject: [PATCH 256/278] Issue #2879892 by blacklabel_tom, drunken monkey: Fixed link in description of "Stemmer" processor. --- CHANGELOG.txt | 2 ++ includes/processor_stemmer.inc | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 39dc9e04..9a4b4265 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2879892 by blacklabel_tom, drunken monkey: Fixed link in description of + "Stemmer" processor. - #2788593 by drunken monkey: Fixed error in Views query settings for specific setups. - #2749963 by drunken monkey: Fixed "Index hierarchy" not having values diff --git a/includes/processor_stemmer.inc b/includes/processor_stemmer.inc index a6c05489..ebb1a9ce 100644 --- a/includes/processor_stemmer.inc +++ b/includes/processor_stemmer.inc @@ -24,12 +24,11 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor { $form = parent::configurationForm(); $args = array( - '!algorithm' => url('https://github.com/markfullmer/porter2'), - '!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'), + '@algorithm' => url('http://snowball.tartarus.org/algorithms/english/stemmer.html'), ); $form += array( 'help' => array( - '#markup' => '

    ' . t('Optionally, provide an exclusion list to override the stemmer algorithm. Read about the algorithm and exclusions.', $args) . '

    ', + '#markup' => '

    ' . t('Optionally, provide an exclusion list to override the stemmer algorithm. (Read about the algorithm.)', $args) . '

    ', ), 'exceptions' => array( '#type' => 'textarea', From a24146afd7af06c41b08c78cdb052ab030dc6c62 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sun, 18 Jun 2017 12:05:16 +0200 Subject: [PATCH 257/278] Issue #1710212 by drunken monkey: Added a data alteration for indexing a user's content. --- CHANGELOG.txt | 2 ++ includes/callback_user_content.inc | 57 ++++++++++++++++++++++++++++++ search_api.info | 1 + search_api.module | 5 +++ 4 files changed, 65 insertions(+) create mode 100644 includes/callback_user_content.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9a4b4265..092eefc6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1710212 by drunken monkey: Added a data alteration for indexing a user's + content. - #2879892 by blacklabel_tom, drunken monkey: Fixed link in description of "Stemmer" processor. - #2788593 by drunken monkey: Fixed error in Views query settings for specific diff --git a/includes/callback_user_content.inc b/includes/callback_user_content.inc new file mode 100644 index 00000000..168f3ae1 --- /dev/null +++ b/includes/callback_user_content.inc @@ -0,0 +1,57 @@ +getEntityType() === 'user'; + } + + /** + * {@inheritdoc} + */ + public function propertyInfo() { + return array( + 'search_api_user_content' => array( + 'label' => t('User content'), + 'description' => t('The nodes created by this user'), + 'type' => 'list', + ), + ); + } + + /** + * {@inheritdoc} + */ + public function alterItems(array &$items) { + $uids = array(); + foreach ($items as $item) { + $uids[] = $item->uid; + } + + $sql = 'SELECT nid, uid FROM {node} WHERE uid IN (:uids)'; + $nids = db_query($sql, array(':uids' => $uids)); + $user_nodes = array(); + foreach ($nids as $row) { + $user_nodes[$row->uid][] = $row->nid; + } + + foreach ($items as $item) { + $item->search_api_user_content = array(); + if (!empty($user_nodes[$item->uid])) { + $item->search_api_user_content = $user_nodes[$item->uid]; + } + } + } + +} diff --git a/search_api.info b/search_api.info index 081fd4e6..0e14ed88 100644 --- a/search_api.info +++ b/search_api.info @@ -16,6 +16,7 @@ files[] = includes/callback_language_control.inc files[] = includes/callback_node_access.inc files[] = includes/callback_node_status.inc files[] = includes/callback_role_filter.inc +files[] = includes/callback_user_content.inc files[] = includes/callback_user_status.inc files[] = includes/datasource.inc files[] = includes/datasource_entity.inc diff --git a/search_api.module b/search_api.module index 61385c5d..3a955fe7 100644 --- a/search_api.module +++ b/search_api.module @@ -1103,6 +1103,11 @@ function search_api_search_api_alter_callback_info() { 'description' => t('Exclude unpublished nodes from the index. Caution: This only affects the indexed nodes themselves. If an enabled node has references to disabled nodes, those will still be indexed (or displayed) normally.'), 'class' => 'SearchApiAlterNodeStatus', ); + $callbacks['search_api_alter_user_content'] = array( + 'name' => t('Add user content'), + 'description' => t('Allows indexing of nodes (and their fields) created by the indexed user. (Caution: This might lead to performance problems, or even errors during indexing, on larger sites.)'), + 'class' => 'SearchApiAlterAddUserContent', + ); $callbacks['search_api_alter_user_status'] = array( 'name' => t('Exclude blocked users'), 'description' => t('Exclude blocked users from the index. Caution: This only affects the indexed users themselves. If an active user account includes a reference to a disabled user, that reference will still be indexed (or displayed) normally.'), From 6bff579f214cd40721f49be53e5a7d18f2bd2b60 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 18 Jul 2017 10:48:34 +0200 Subject: [PATCH 258/278] Adapted CHANGELOG.txt to 1.22 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 092eefc6..5415ddbd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.22 (2017-07-18): +----------------------------- - #1710212 by drunken monkey: Added a data alteration for indexing a user's content. - #2879892 by blacklabel_tom, drunken monkey: Fixed link in description of From 8b46943fc26a740d25431b25d56e2f58a0186e27 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 18 Jul 2017 10:48:34 +0200 Subject: [PATCH 259/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5415ddbd..ecdae8e6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.22 (2017-07-18): ----------------------------- - #1710212 by drunken monkey: Added a data alteration for indexing a user's From b9b5fdaf52f65909a6abb8c89d1167200af82a70 Mon Sep 17 00:00:00 2001 From: pobster Date: Wed, 6 Sep 2017 17:13:36 +0200 Subject: [PATCH 260/278] Issue #2904268 by pobster, drunken monkey: Added support for language hierarchy in Views. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_language.inc | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ecdae8e6..0128bc18 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2904268 by pobster, drunken monkey: Added support for language hierarchy in + Views. Search API 1.22 (2017-07-18): ----------------------------- diff --git a/contrib/search_api_views/includes/handler_filter_language.inc b/contrib/search_api_views/includes/handler_filter_language.inc index a7de5f94..3202cdbc 100644 --- a/contrib/search_api_views/includes/handler_filter_language.inc +++ b/contrib/search_api_views/includes/handler_filter_language.inc @@ -18,10 +18,13 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt */ protected function get_value_options() { parent::get_value_options(); - $this->value_options = array( - 'current' => t("Current user's language"), - 'default' => t('Default site language'), - ) + $this->value_options; + $options = array(); + if (module_exists('language_hierarchy')) { + $options['fallback'] = t("Current user's language with fallback"); + } + $options['current'] = t("Current user's language"); + $options['default'] = t('Default site language'); + $this->value_options = $options + $this->value_options; } /** @@ -40,6 +43,11 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt elseif ($v == 'default') { $this->value[$i] = language_default('language'); } + elseif ($v == 'fallback' && module_exists('language_hierarchy')) { + $fallbacks = array($language_content->language => $language_content->language); + $fallbacks += array_keys(language_hierarchy_get_ancestors($language_content->language)); + $this->value[$i] = drupal_map_assoc($fallbacks); + } } parent::query(); } From f838690d8d3e94f1166af38484bcdea49e1ab7e3 Mon Sep 17 00:00:00 2001 From: ciss Date: Sun, 10 Sep 2017 16:46:07 +0200 Subject: [PATCH 261/278] Issue #2905445 by ciss, drunken monkey: Fixed error handling in Views term filter handler. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_taxonomy_term.inc | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0128bc18..c3164e29 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2905445 by ciss, drunken monkey: Fixed error handling in Views term filter + handler. - #2904268 by pobster, drunken monkey: Added support for language hierarchy in Views. diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc index f3317cfa..51982b0e 100644 --- a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc +++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -321,9 +321,13 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte * {@inheritdoc} */ protected function ids_to_strings(array $ids) { + $ids = array_filter($ids); + if (!$ids) { + return ''; + } return implode(', ', db_select('taxonomy_term_data', 'td') ->fields('td', array('name')) - ->condition('td.tid', array_filter($ids)) + ->condition('td.tid', $ids) ->execute() ->fetchCol()); } From 6b10116dddcad0113410dce38616994bb302bf70 Mon Sep 17 00:00:00 2001 From: DylanDonkersgoed Date: Mon, 4 Dec 2017 12:25:52 +0100 Subject: [PATCH 262/278] Issue #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: Added support for the "Content access" processor for "Multiple types" indexes. --- CHANGELOG.txt | 2 + includes/callback_comment_access.inc | 21 +++- includes/callback_node_access.inc | 27 ++++- includes/callback_node_status.inc | 43 +++++++- search_api.module | 152 ++++++++++++++++++++------- 5 files changed, 197 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c3164e29..739f572b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: Added support + for the "Content access" processor for "Multiple types" indexes. - #2905445 by ciss, drunken monkey: Fixed error handling in Views term filter handler. - #2904268 by pobster, drunken monkey: Added support for language hierarchy in diff --git a/includes/callback_comment_access.inc b/includes/callback_comment_access.inc index e6273530..6fbe8323 100644 --- a/includes/callback_comment_access.inc +++ b/includes/callback_comment_access.inc @@ -12,10 +12,16 @@ class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess { /** * Overrides SearchApiAlterNodeAccess::supportsIndex(). * - * Returns TRUE only for indexes on comments. + * Returns TRUE only for indexes on comments or multi-entity indexes that + * include comments. */ public function supportsIndex(SearchApiIndex $index) { - return $index->getEntityType() === 'comment'; + if ($this->isMultiEntityIndex($index)) { + return in_array('comment', $index->options['datasource']['types']); + } + else { + return $index->getEntityType() == 'comment'; + } } /** @@ -24,7 +30,16 @@ class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess { * Returns the comment's node, instead of the item (i.e., the comment) itself. */ protected function getNode($item) { - return node_load($item->nid); + if ($this->isMultiEntityIndex()) { + if ($item->item_type !== 'comment') { + return NULL; + } + $nid = $item->comment->nid; + } + else { + $nid = $item->nid; + } + return node_load($nid); } /** diff --git a/includes/callback_node_access.inc b/includes/callback_node_access.inc index 8bfab494..93775664 100644 --- a/includes/callback_node_access.inc +++ b/includes/callback_node_access.inc @@ -12,11 +12,17 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { /** * Overrides SearchApiAbstractAlterCallback::supportsIndex(). * - * Returns TRUE only for indexes on nodes. + * Returns TRUE only for indexes on nodes or multi-entity indexes that include + * nodes. */ public function supportsIndex(SearchApiIndex $index) { // Currently only node access is supported. - return $index->getEntityType() === 'node'; + if ($this->isMultiEntityIndex($index)) { + return in_array('node', $index->options['datasource']['types']); + } + else { + return $index->getEntityType() == 'node'; + } } /** @@ -47,6 +53,12 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { foreach ($items as $id => $item) { $node = $this->getNode($item); + + // Skip non-node items. + if (empty($node)) { + continue; + } + // Check whether all users have access to the node. if (!node_access('view', $node, $account)) { // Get node access grants. @@ -71,8 +83,19 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { * In the default implementation for nodes, the item is already the node. * Subclasses may override this to easily provide node access checks for * items related to nodes. + * + * @param $item + * The item which is being indexed. + * + * @return + * Either the node object, or NULL if the current item is not a node. */ protected function getNode($item) { + if ($this->isMultiEntityIndex()) { + // For multi-entity indexes, check whether the item is a node. + return $item->item_type == 'node' ? $item->node : NULL; + } + // Normal node index: the whole item is the node. return $item; } diff --git a/includes/callback_node_status.inc b/includes/callback_node_status.inc index ee57c387..278ad1b3 100644 --- a/includes/callback_node_status.inc +++ b/includes/callback_node_status.inc @@ -22,7 +22,12 @@ class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback { * TRUE if the callback can run on the given index; FALSE otherwise. */ public function supportsIndex(SearchApiIndex $index) { - return $index->getEntityType() === 'node'; + if ($this->isMultiEntityIndex($index)) { + return in_array('node', $index->options['datasource']['types']); + } + else { + return $index->getEntityType() == 'node'; + } } /** @@ -35,11 +40,41 @@ class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback { * An array of items to be altered, keyed by item IDs. */ public function alterItems(array &$items) { - foreach ($items as $nid => &$item) { - if (empty($item->status)) { - unset($items[$nid]); + foreach ($items as $id => $item) { + $node = $this->getNode($item); + + // Skip non-node items. + if (!empty($node) && empty($node->status)) { + unset($items[$id]); } } } + /** + * Retrieves the node related to a search item. + * + * @param object $item + * The item which is being indexed. + * + * @return object|null + * Either the node object, or NULL if the current item is not a node. + */ + protected function getNode($item) { + if ($this->isMultiEntityIndex()) { + // For multi-entity indexes, check the item is a node. + if ($item->item_type == 'node') { + // The node is stored on a property of the item. + return $item->node; + } + else { + // Not a node. + return NULL; + } + } + else { + // Normal node index: the whole item is the node. + return $item; + } + } + } diff --git a/search_api.module b/search_api.module index 3a955fe7..98760ab6 100644 --- a/search_api.module +++ b/search_api.module @@ -2051,17 +2051,32 @@ function search_api_get_processors() { function search_api_search_api_query_alter(SearchApiQueryInterface $query) { global $user; $index = $query->getIndex(); + // Only add node access if the necessary fields are indexed in the index, and // unless disabled explicitly by the query. - $type = $index->getEntityType(); - if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status']) && !$query->getOption('search_api_bypass_access')) { + $types = array(); + if ($index->getEntityType()) { + $types[] = $index->getEntityType(); + } + if (!empty($index->options['datasource']['types'])) { + $types += $index->options['datasource']['types']; + } + $data_alter_callback_exists = FALSE; + foreach ($types as $type) { + if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status'])) { + $data_alter_callback_exists = TRUE; + break; + } + } + + if ($data_alter_callback_exists && !$query->getOption('search_api_bypass_access')) { $account = $query->getOption('search_api_access_account', $user); if (is_numeric($account)) { $account = user_load($account); } if (is_object($account)) { try { - _search_api_query_add_node_access($account, $query, $type); + _search_api_query_add_node_access($account, $query); } catch (SearchApiException $e) { watchdog_exception('search_api', $e); @@ -2087,66 +2102,125 @@ function search_api_search_api_query_alter(SearchApiQueryInterface $query) { * The user object, who searches. * @param SearchApiQueryInterface $query * The query to which a node access filter should be added, if applicable. - * @param string $type - * (optional) The type of search – either "node" or "comment". Defaults to - * "node". * * @throws SearchApiException * If not all necessary fields are indexed on the index. */ -function _search_api_query_add_node_access($account, SearchApiQueryInterface $query, $type = 'node') { +function _search_api_query_add_node_access($account, SearchApiQueryInterface $query) { // Don't do anything if the user can access all content. if (user_access('bypass node access', $account)) { return; } - $is_comment = ($type == 'comment'); + $affected_types = array(); + $unaffected_types = array(); + $status_field = NULL; - // Check whether the necessary fields are indexed. - $fields = $query->getIndex()->options['fields']; - $required = array('search_api_access_node', 'status'); - if (!$is_comment) { - $required[] = 'author'; - } - foreach ($required as $field) { - if (empty($fields[$field])) { - $vars['@field'] = $field; - $vars['@index'] = $query->getIndex()->name; - throw new SearchApiException(t('Required field @field not indexed on index @index. Could not perform access checks.', $vars)); + $index = $query->getIndex(); + // Gather the affected and unaffected entity types. + if ($index->item_type === 'multiple') { + $all_types = array(); + if (isset($index->options['datasource']['types'])) { + $all_types = drupal_map_assoc($index->options['datasource']['types']); + } + foreach ($all_types as $type) { + if (in_array($type, array('node', 'comment'))) { + $affected_types[$type] = $type; + $status_field = "$type:status"; + } + else { + $unaffected_types[$type] = $type; + } } } - - // If the user cannot access content/comments at all, return no results. - if (!user_access('access content', $account) || ($is_comment && !user_access('access comments', $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_NOTICE); + elseif ($index->item_type === 'node') { + $affected_types['node'] = 'node'; + } + elseif ($index->item_type === 'comment') { + $affected_types['comment'] = 'comment'; + } + // Unaffected type. Do nothing. + else { return; } - // Filter by the "published" status. - $published = $is_comment ? COMMENT_PUBLISHED : NODE_PUBLISHED; - if (!$is_comment && user_access('view own unpublished content')) { - $filter = $query->createFilter('OR'); - $filter->condition('status', $published); - $filter->condition('author', $account->uid); - $query->filter($filter); + // The filter structure we want looks like this: + // [belongs to other datasource] + // OR + // ( + // [is enabled (or was created by the user, if applicable)] + // AND + // [grants view access to one of the user's gid/realm combinations] + // ) + // If there are no "other" datasources, we don't need the nested OR, + // however, and can add the inner conditions directly to the query. + if ($unaffected_types) { + $outer_conditions = $query->createFilter('OR', array('content_access')); + $query->filter($outer_conditions); + foreach ($unaffected_types as $type) { + $outer_conditions->condition('item_type', $type); + } + $access_conditions = $query->createFilter('AND'); + $outer_conditions->filter($access_conditions); } else { - $query->condition('status', $published); + $access_conditions = $query; + } + + if (!user_access('access content', $account)) { + unset($affected_types['node']); + } + if (!user_access('access comments', $account)) { + unset($affected_types['comment']); + } + + // If the user does not have the permission to see any content at all, deny + // access to all items from affected datasources. + if (!$affected_types) { + // If there were "other" datasources, the existing filter will already + // remove all results of node or comment datasources. Otherwise, we should + // not return any results at all. + if (!$unaffected_types) { + // Simple hack for returning no results. + $query->condition($status_field, 0); + $query->condition($status_field, 1); + watchdog('search_api', 'User @name tried to execute a search, but cannot access content.', array('@name' => theme('username', array('account' => $account))), WATCHDOG_NOTICE); + + } + return; + } + + // Collect all the required fields that need to be part of the index. + $unpublished_own = user_access('view own unpublished content', $account); + + $enabled_conditions = $query->createFilter('OR', array('content_access_enabled')); + foreach ($affected_types as $entity_type) { + // If this is a comment datasource, or users cannot view their own + // unpublished nodes, a simple filter on "status" is enough. Otherwise, + // it's a bit more complicated. + $status_field = $entity_type . ':status'; + if ($status_field) { + $enabled_conditions->condition($status_field, '1'); + } + if ($entity_type == 'node' && $unpublished_own) { + $author_field = $entity_type . ':author'; + if ($author_field) { + $enabled_conditions->condition($author_field, $account->uid); + } + } } - // Filter by node access grants. - $filter = $query->createFilter('OR'); + $access_conditions->filter($enabled_conditions); + + // Filter by the user's node access grants. + $grants_conditions = $query->createFilter('OR', array('content_access_grants')); $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"); + $grants_conditions->condition('search_api_access_node', "node_access_$realm:$gid"); } } - $filter->condition('search_api_access_node', 'node_access__all'); - $query->filter($filter); + $access_conditions->filter($grants_conditions); } /** From 40d143ff699492d6ba012c15e60fa669262224ae Mon Sep 17 00:00:00 2001 From: jannis Date: Mon, 18 Dec 2017 15:26:51 +0100 Subject: [PATCH 263/278] Issue #2928769 by jannis, drunken monkey: Fixed Views cache not being cleared when enabling indexes. --- CHANGELOG.txt | 2 ++ contrib/search_api_views/search_api_views.module | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 739f572b..08ba0b63 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2928769 by jannis, drunken monkey: Fixed Views cache not being cleared when + enabling indexes. - #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: Added support for the "Content access" processor for "Multiple types" indexes. - #2905445 by ciss, drunken monkey: Fixed error handling in Views term filter diff --git a/contrib/search_api_views/search_api_views.module b/contrib/search_api_views/search_api_views.module index 62fcb944..a927f082 100644 --- a/contrib/search_api_views/search_api_views.module +++ b/contrib/search_api_views/search_api_views.module @@ -27,8 +27,11 @@ function search_api_views_search_api_index_insert() { */ function search_api_views_search_api_index_update(SearchApiIndex $index) { // Check whether index was disabled. - if (!$index->enabled && $index->original->enabled) { + $is_enabled = $index->enabled; + $was_enabled = $index->original->enabled; + if (!$is_enabled && $was_enabled) { _search_api_views_index_unavailable($index); + return; } // Check whether the indexed fields changed. @@ -36,7 +39,9 @@ function search_api_views_search_api_index_update(SearchApiIndex $index) { $old_fields = $old_fields['fields']; $new_fields = $index->options + array('fields' => array()); $new_fields = $new_fields['fields']; - if ($old_fields != $new_fields) { + + // If the index was enabled or its fields changed, invalidate the Views cache. + if ($is_enabled != $was_enabled || $old_fields != $new_fields) { views_invalidate_cache(); } } From bd628b181699d0f10fc4ca9ce9511b247cf28824 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Tue, 19 Dec 2017 18:17:03 +0100 Subject: [PATCH 264/278] Issue #2927692 by drunken monkey, Kristi Wachter: Fixed exposed grouped Views options filters. --- CHANGELOG.txt | 2 ++ .../includes/handler_filter_options.inc | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 08ba0b63..ceed3b46 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2927692 by drunken monkey, Kristi Wachter: Fixed exposed grouped Views + options filters. - #2928769 by jannis, drunken monkey: Fixed Views cache not being cleared when enabling indexes. - #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: Added support diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc index c63c07e7..3040fb0f 100644 --- a/contrib/search_api_views/includes/handler_filter_options.inc +++ b/contrib/search_api_views/includes/handler_filter_options.inc @@ -256,6 +256,32 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { return $operator . (($values !== '') ? ' ' . $values : ''); } + /** + * {@inheritdoc} + */ + function accept_exposed_input($input) { + $accepted = parent::accept_exposed_input($input); + + // Grouped filters will have the raw form values structure from the + // checkboxes as the value here. Convert that into the correct array of + // values instead. + if ($accepted && is_array($this->value) && $this->is_a_group()) { + // For some reason, Views thinks it's a good idea to nest the form values + // into a second array in some cases. That one will be numerically indexed + // with just a single entry, though, so it should be relatively easy to + // spot. + if (count($this->value) && isset($this->value[0])) { + $this->value = reset($this->value); + } + $this->value = array_keys(array_filter($this->value)); + if (!$this->value) { + return FALSE; + } + } + + return $accepted; + } + /** * Add this filter to the query. */ From 0d8fc5e90e761868472911e1beaec95cd556b2a3 Mon Sep 17 00:00:00 2001 From: xlyz Date: Sat, 20 Jan 2018 11:40:15 +0100 Subject: [PATCH 265/278] Issue #1393064 by xlyz, drunken monkey, jannis: Fixed handling of empty facet filters. --- CHANGELOG.txt | 2 ++ .../search_api_facetapi/plugins/facetapi/query_type_term.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ceed3b46..7fd7ce45 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1393064 by xlyz, drunken monkey, jannis: Fixed handling of empty facet + filters. - #2927692 by drunken monkey, Kristi Wachter: Fixed exposed grouped Views options filters. - #2928769 by jannis, drunken monkey: Fixed Views cache not being cleared when diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 64d797a7..4c674a8a 100644 --- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -115,7 +115,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy if ($filter == '!') { $query_filter->condition($field, NULL, $exclude ? '<>' : '='); } - elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) { + elseif ($filter && $filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) { $lower = trim(substr($filter, 1, $pos)); $upper = trim(substr($filter, $pos + 4, -1)); if ($lower == '*' && $upper == '*') { From 9fa46446762b4f35b1514174542b3f88e372685f Mon Sep 17 00:00:00 2001 From: kevineinarsson Date: Sun, 21 Jan 2018 16:04:42 +0100 Subject: [PATCH 266/278] Issue #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: Fixed highlighting for text with multi-byte characters. --- CHANGELOG.txt | 2 ++ includes/processor_highlight.inc | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7fd7ce45..9ad4ce3d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: Fixed + highlighting for text with multi-byte characters. - #1393064 by xlyz, drunken monkey, jannis: Fixed handling of empty facet filters. - #2927692 by drunken monkey, Kristi Wachter: Fixed exposed grouped Views diff --git a/includes/processor_highlight.inc b/includes/processor_highlight.inc index 5b419491..04be02b6 100644 --- a/includes/processor_highlight.inc +++ b/includes/processor_highlight.inc @@ -351,7 +351,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor { } } else { - $p = stripos($text, $key, $included[$key]); + $function = function_exists('mb_stripos') ? 'mb_stripos' : 'stripos'; + $p = $function($text, $key, $included[$key]); } // Now locate a space in front (position $q) and behind it (position $s), // leaving about 60 characters extra before and after for context. From eb15a801158acfc74bebfcfbac1fe60dd3bf81a8 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 18 Feb 2018 23:34:23 +0100 Subject: [PATCH 267/278] Issue #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors at feature module installation in certain edge cases. --- CHANGELOG.txt | 2 + search_api.module | 22 ++++++ search_api.test | 9 ++- tests/search_api_test_2.info | 10 +++ tests/search_api_test_2.module | 136 +++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 tests/search_api_test_2.info create mode 100644 tests/search_api_test_2.module diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9ad4ce3d..f637b345 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors at feature + module installation in certain edge cases. - #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: Fixed highlighting for text with multi-byte characters. - #1393064 by xlyz, drunken monkey, jannis: Fixed handling of empty facet diff --git a/search_api.module b/search_api.module index 98760ab6..53d00bd0 100644 --- a/search_api.module +++ b/search_api.module @@ -1025,6 +1025,28 @@ function search_api_search_api_item_type_info() { return $types; } +/** + * Implements hook_module_implements_alter(). + * + * Ensures the item type and service class static caches are invalidated at the + * right time. + */ +function search_api_module_implements_alter(array &$implementations, $hook) { + switch ($hook) { + case 'modules_enabled': + $group = $implementations['search_api']; + unset($implementations['search_api']); + $implementations = array('search_api' => $group) + $implementations; + break; + + case 'modules_disabled': + $group = $implementations['search_api']; + unset($implementations['search_api']); + $implementations['search_api'] = $group; + break; + } +} + /** * Implements hook_modules_enabled(). */ diff --git a/search_api.test b/search_api.test index 23f36bcf..76a89705 100644 --- a/search_api.test +++ b/search_api.test @@ -86,6 +86,7 @@ class SearchApiWebTest extends DrupalWebTestCase { * and then run tests on it. */ public function testFramework() { + module_enable(array('search_api_test_2')); $this->drupalLogin($this->drupalCreateUser(array('administer search_api'))); $this->insertItems(); $this->createIndex(); @@ -730,13 +731,17 @@ class SearchApiWebTest extends DrupalWebTestCase { * deleteServer()) and that all associated tables and variables are removed. */ protected function disableModules() { + module_disable(array('search_api_test_2'), FALSE); + $this->assertFalse(module_exists('search_api_test_2'), 'Second test module was successfully disabled.'); module_disable(array('search_api_test'), FALSE); - $this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.'); + $this->assertFalse(module_exists('search_api_test'), 'First test module was successfully disabled.'); module_disable(array('search_api'), FALSE); $this->assertFalse(module_exists('search_api'), 'Search API module was successfully disabled.'); + drupal_uninstall_modules(array('search_api_test_2'), FALSE); + $this->assertEqual(drupal_get_installed_schema_version('search_api_test_2', TRUE), SCHEMA_UNINSTALLED, 'Second test module was successfully uninstalled.'); drupal_uninstall_modules(array('search_api_test'), FALSE); - $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'Test module was successfully uninstalled.'); + $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'First test module was successfully uninstalled.'); $this->assertFalse(db_table_exists('search_api_test'), 'Test module table was successfully removed.'); drupal_uninstall_modules(array('search_api'), FALSE); $this->assertEqual(drupal_get_installed_schema_version('search_api', TRUE), SCHEMA_UNINSTALLED, 'Search API module was successfully uninstalled.'); diff --git a/tests/search_api_test_2.info b/tests/search_api_test_2.info new file mode 100644 index 00000000..a3beff1f --- /dev/null +++ b/tests/search_api_test_2.info @@ -0,0 +1,10 @@ +name = Search API test service 2 +description = "A module providing a second test search service." +core = 7.x +package = Search + +dependencies[] = search_api + +files[] = search_api_test_service_2.module + +hidden = TRUE diff --git a/tests/search_api_test_2.module b/tests/search_api_test_2.module new file mode 100644 index 00000000..d78a80eb --- /dev/null +++ b/tests/search_api_test_2.module @@ -0,0 +1,136 @@ + $name, + 'description' => 'search_api_test_service_2 description', + 'class' => 'SearchApiDummyService', + ); + return $services; +} + +/** + * Implements hook_default_search_api_server(). + */ +function search_api_test_2_default_search_api_server() { + $id = 'test_server_2'; + $items[$id] = entity_create('search_api_server', array( + 'name' => 'Search API test server 2', + 'machine_name' => $id, + 'enabled' => 1, + 'description' => 'A server used for testing.', + 'class' => 'search_api_test_service_2', + )); + return $items; +} + +/** + * Dummy service for testing. + */ +class SearchApiDummyService implements SearchApiServiceInterface { + + /** + * {@inheritdoc} + */ + public function __construct(\SearchApiServer $server) {} + + /** + * {@inheritdoc} + */ + public function configurationForm(array $form, array &$form_state) { + return array(); + } + + /** + * {@inheritdoc} + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state) {} + + /** + * {@inheritdoc} + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) {} + + /** + * {@inheritdoc} + */ + public function supportsFeature($feature) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function viewSettings() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function postCreate() {} + + /** + * {@inheritdoc} + */ + public function postUpdate() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function preDelete() {} + + /** + * {@inheritdoc} + */ + public function addIndex(SearchApiIndex $index) {} + + /** + * {@inheritdoc} + */ + public function fieldsUpdated(SearchApiIndex $index) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function removeIndex($index) {} + + /** + * {@inheritdoc} + */ + public function indexItems(SearchApiIndex $index, array $items) { + return array(); + } + + /** + * {@inheritdoc} + */ + public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {} + + /** + * {@inheritdoc} + */ + public function query(SearchApiIndex $index, $options = array()) { + throw new SearchApiException("The dummy service doesn't support queries"); + } + + /** + * {@inheritdoc} + */ + public function search(SearchApiQueryInterface $query) { + return array(); + } +} From 10544410ff72eda3a2f106e50596d161ad52e883 Mon Sep 17 00:00:00 2001 From: damienmckenna Date: Sun, 4 Mar 2018 14:36:17 +0100 Subject: [PATCH 268/278] Issue #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of multi-word tokens. --- CHANGELOG.txt | 2 ++ includes/processor_stemmer.inc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f637b345..f3622424 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of multi-word + tokens. - #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors at feature module installation in certain edge cases. - #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: Fixed diff --git a/includes/processor_stemmer.inc b/includes/processor_stemmer.inc index ebb1a9ce..03d9fb03 100644 --- a/includes/processor_stemmer.inc +++ b/includes/processor_stemmer.inc @@ -65,7 +65,7 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor { $stemmed[] = $word; } } - $value = implode('', $stemmed); + $value = implode(' ', $stemmed); } /** From eec18388efea4f454f50f4ac7c97f53027cd2e66 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 31 Mar 2018 20:32:45 +0200 Subject: [PATCH 269/278] Adapted CHANGELOG.txt to 1.23 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f3622424..b1fd5d28 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.23 (2018-03-31): +----------------------------- - #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of multi-word tokens. - #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors at feature From 2744b4e417e0f19e1a3741c8296f44a93776542e Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 31 Mar 2018 20:32:45 +0200 Subject: [PATCH 270/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b1fd5d28..57457676 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.23 (2018-03-31): ----------------------------- - #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of multi-word From 66a8733985eee74d43ea2c7e840d9923c814e613 Mon Sep 17 00:00:00 2001 From: jcnventura Date: Thu, 5 Apr 2018 09:30:58 +0200 Subject: [PATCH 271/278] Issue #2958201 by jcnventura, drunken monkey: Reverted issue #2566529: Added support for the "Content access" processor for "Multiple types" indexes. --- CHANGELOG.txt | 2 + includes/callback_comment_access.inc | 21 +--- includes/callback_node_access.inc | 27 +---- includes/callback_node_status.inc | 43 +------- search_api.module | 152 +++++++-------------------- 5 files changed, 50 insertions(+), 195 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 57457676..492fbd4e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2958201 by jcnventura, drunken monkey: Reverted issue #2566529: Added + support for the "Content access" processor for "Multiple types" indexes. Search API 1.23 (2018-03-31): ----------------------------- diff --git a/includes/callback_comment_access.inc b/includes/callback_comment_access.inc index 6fbe8323..e6273530 100644 --- a/includes/callback_comment_access.inc +++ b/includes/callback_comment_access.inc @@ -12,16 +12,10 @@ class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess { /** * Overrides SearchApiAlterNodeAccess::supportsIndex(). * - * Returns TRUE only for indexes on comments or multi-entity indexes that - * include comments. + * Returns TRUE only for indexes on comments. */ public function supportsIndex(SearchApiIndex $index) { - if ($this->isMultiEntityIndex($index)) { - return in_array('comment', $index->options['datasource']['types']); - } - else { - return $index->getEntityType() == 'comment'; - } + return $index->getEntityType() === 'comment'; } /** @@ -30,16 +24,7 @@ class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess { * Returns the comment's node, instead of the item (i.e., the comment) itself. */ protected function getNode($item) { - if ($this->isMultiEntityIndex()) { - if ($item->item_type !== 'comment') { - return NULL; - } - $nid = $item->comment->nid; - } - else { - $nid = $item->nid; - } - return node_load($nid); + return node_load($item->nid); } /** diff --git a/includes/callback_node_access.inc b/includes/callback_node_access.inc index 93775664..8bfab494 100644 --- a/includes/callback_node_access.inc +++ b/includes/callback_node_access.inc @@ -12,17 +12,11 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { /** * Overrides SearchApiAbstractAlterCallback::supportsIndex(). * - * Returns TRUE only for indexes on nodes or multi-entity indexes that include - * nodes. + * Returns TRUE only for indexes on nodes. */ public function supportsIndex(SearchApiIndex $index) { // Currently only node access is supported. - if ($this->isMultiEntityIndex($index)) { - return in_array('node', $index->options['datasource']['types']); - } - else { - return $index->getEntityType() == 'node'; - } + return $index->getEntityType() === 'node'; } /** @@ -53,12 +47,6 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { foreach ($items as $id => $item) { $node = $this->getNode($item); - - // Skip non-node items. - if (empty($node)) { - continue; - } - // Check whether all users have access to the node. if (!node_access('view', $node, $account)) { // Get node access grants. @@ -83,19 +71,8 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback { * In the default implementation for nodes, the item is already the node. * Subclasses may override this to easily provide node access checks for * items related to nodes. - * - * @param $item - * The item which is being indexed. - * - * @return - * Either the node object, or NULL if the current item is not a node. */ protected function getNode($item) { - if ($this->isMultiEntityIndex()) { - // For multi-entity indexes, check whether the item is a node. - return $item->item_type == 'node' ? $item->node : NULL; - } - // Normal node index: the whole item is the node. return $item; } diff --git a/includes/callback_node_status.inc b/includes/callback_node_status.inc index 278ad1b3..ee57c387 100644 --- a/includes/callback_node_status.inc +++ b/includes/callback_node_status.inc @@ -22,12 +22,7 @@ class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback { * TRUE if the callback can run on the given index; FALSE otherwise. */ public function supportsIndex(SearchApiIndex $index) { - if ($this->isMultiEntityIndex($index)) { - return in_array('node', $index->options['datasource']['types']); - } - else { - return $index->getEntityType() == 'node'; - } + return $index->getEntityType() === 'node'; } /** @@ -40,41 +35,11 @@ class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback { * An array of items to be altered, keyed by item IDs. */ public function alterItems(array &$items) { - foreach ($items as $id => $item) { - $node = $this->getNode($item); - - // Skip non-node items. - if (!empty($node) && empty($node->status)) { - unset($items[$id]); + foreach ($items as $nid => &$item) { + if (empty($item->status)) { + unset($items[$nid]); } } } - /** - * Retrieves the node related to a search item. - * - * @param object $item - * The item which is being indexed. - * - * @return object|null - * Either the node object, or NULL if the current item is not a node. - */ - protected function getNode($item) { - if ($this->isMultiEntityIndex()) { - // For multi-entity indexes, check the item is a node. - if ($item->item_type == 'node') { - // The node is stored on a property of the item. - return $item->node; - } - else { - // Not a node. - return NULL; - } - } - else { - // Normal node index: the whole item is the node. - return $item; - } - } - } diff --git a/search_api.module b/search_api.module index 53d00bd0..8fe3e2a7 100644 --- a/search_api.module +++ b/search_api.module @@ -2073,32 +2073,17 @@ function search_api_get_processors() { function search_api_search_api_query_alter(SearchApiQueryInterface $query) { global $user; $index = $query->getIndex(); - // Only add node access if the necessary fields are indexed in the index, and // unless disabled explicitly by the query. - $types = array(); - if ($index->getEntityType()) { - $types[] = $index->getEntityType(); - } - if (!empty($index->options['datasource']['types'])) { - $types += $index->options['datasource']['types']; - } - $data_alter_callback_exists = FALSE; - foreach ($types as $type) { - if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status'])) { - $data_alter_callback_exists = TRUE; - break; - } - } - - if ($data_alter_callback_exists && !$query->getOption('search_api_bypass_access')) { + $type = $index->getEntityType(); + if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status']) && !$query->getOption('search_api_bypass_access')) { $account = $query->getOption('search_api_access_account', $user); if (is_numeric($account)) { $account = user_load($account); } if (is_object($account)) { try { - _search_api_query_add_node_access($account, $query); + _search_api_query_add_node_access($account, $query, $type); } catch (SearchApiException $e) { watchdog_exception('search_api', $e); @@ -2124,125 +2109,66 @@ function search_api_search_api_query_alter(SearchApiQueryInterface $query) { * The user object, who searches. * @param SearchApiQueryInterface $query * The query to which a node access filter should be added, if applicable. + * @param string $type + * (optional) The type of search – either "node" or "comment". Defaults to + * "node". * * @throws SearchApiException * If not all necessary fields are indexed on the index. */ -function _search_api_query_add_node_access($account, SearchApiQueryInterface $query) { +function _search_api_query_add_node_access($account, SearchApiQueryInterface $query, $type = 'node') { // Don't do anything if the user can access all content. if (user_access('bypass node access', $account)) { return; } - $affected_types = array(); - $unaffected_types = array(); - $status_field = NULL; + $is_comment = ($type == 'comment'); - $index = $query->getIndex(); - // Gather the affected and unaffected entity types. - if ($index->item_type === 'multiple') { - $all_types = array(); - if (isset($index->options['datasource']['types'])) { - $all_types = drupal_map_assoc($index->options['datasource']['types']); - } - foreach ($all_types as $type) { - if (in_array($type, array('node', 'comment'))) { - $affected_types[$type] = $type; - $status_field = "$type:status"; - } - else { - $unaffected_types[$type] = $type; - } - } - } - elseif ($index->item_type === 'node') { - $affected_types['node'] = 'node'; + // Check whether the necessary fields are indexed. + $fields = $query->getIndex()->options['fields']; + $required = array('search_api_access_node', 'status'); + if (!$is_comment) { + $required[] = 'author'; } - elseif ($index->item_type === 'comment') { - $affected_types['comment'] = 'comment'; + foreach ($required as $field) { + if (empty($fields[$field])) { + $vars['@field'] = $field; + $vars['@index'] = $query->getIndex()->name; + throw new SearchApiException(t('Required field @field not indexed on index @index. Could not perform access checks.', $vars)); + } } - // Unaffected type. Do nothing. - else { + + // If the user cannot access content/comments at all, return no results. + if (!user_access('access content', $account) || ($is_comment && !user_access('access comments', $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_NOTICE); return; } - // The filter structure we want looks like this: - // [belongs to other datasource] - // OR - // ( - // [is enabled (or was created by the user, if applicable)] - // AND - // [grants view access to one of the user's gid/realm combinations] - // ) - // If there are no "other" datasources, we don't need the nested OR, - // however, and can add the inner conditions directly to the query. - if ($unaffected_types) { - $outer_conditions = $query->createFilter('OR', array('content_access')); - $query->filter($outer_conditions); - foreach ($unaffected_types as $type) { - $outer_conditions->condition('item_type', $type); - } - $access_conditions = $query->createFilter('AND'); - $outer_conditions->filter($access_conditions); + // Filter by the "published" status. + $published = $is_comment ? COMMENT_PUBLISHED : NODE_PUBLISHED; + if (!$is_comment && user_access('view own unpublished content')) { + $filter = $query->createFilter('OR'); + $filter->condition('status', $published); + $filter->condition('author', $account->uid); + $query->filter($filter); } else { - $access_conditions = $query; - } - - if (!user_access('access content', $account)) { - unset($affected_types['node']); - } - if (!user_access('access comments', $account)) { - unset($affected_types['comment']); - } - - // If the user does not have the permission to see any content at all, deny - // access to all items from affected datasources. - if (!$affected_types) { - // If there were "other" datasources, the existing filter will already - // remove all results of node or comment datasources. Otherwise, we should - // not return any results at all. - if (!$unaffected_types) { - // Simple hack for returning no results. - $query->condition($status_field, 0); - $query->condition($status_field, 1); - watchdog('search_api', 'User @name tried to execute a search, but cannot access content.', array('@name' => theme('username', array('account' => $account))), WATCHDOG_NOTICE); - - } - return; - } - - // Collect all the required fields that need to be part of the index. - $unpublished_own = user_access('view own unpublished content', $account); - - $enabled_conditions = $query->createFilter('OR', array('content_access_enabled')); - foreach ($affected_types as $entity_type) { - // If this is a comment datasource, or users cannot view their own - // unpublished nodes, a simple filter on "status" is enough. Otherwise, - // it's a bit more complicated. - $status_field = $entity_type . ':status'; - if ($status_field) { - $enabled_conditions->condition($status_field, '1'); - } - if ($entity_type == 'node' && $unpublished_own) { - $author_field = $entity_type . ':author'; - if ($author_field) { - $enabled_conditions->condition($author_field, $account->uid); - } - } + $query->condition('status', $published); } - $access_conditions->filter($enabled_conditions); - - // Filter by the user's node access grants. - $grants_conditions = $query->createFilter('OR', array('content_access_grants')); + // Filter by node access grants. + $filter = $query->createFilter('OR'); $grants = node_access_grants('view', $account); foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { - $grants_conditions->condition('search_api_access_node', "node_access_$realm:$gid"); + $filter->condition('search_api_access_node', "node_access_$realm:$gid"); } } - $access_conditions->filter($grants_conditions); + $filter->condition('search_api_access_node', 'node_access__all'); + $query->filter($filter); } /** From 2c89c8ee4be7140bf7106de5be089efe12d350d9 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 5 Apr 2018 09:31:19 +0200 Subject: [PATCH 272/278] Adapted CHANGELOG.txt to 1.24 release. --- CHANGELOG.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 492fbd4e..24c120fe 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,5 @@ -Search API 1.x, dev (xxxx-xx-xx): ---------------------------------- +Search API 1.24 (2018-04-05): +----------------------------- - #2958201 by jcnventura, drunken monkey: Reverted issue #2566529: Added support for the "Content access" processor for "Multiple types" indexes. From 91747cebe53b95faf18c5f4a79c8fb30e2da8de8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Thu, 5 Apr 2018 09:31:19 +0200 Subject: [PATCH 273/278] Back to dev version. --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 24c120fe..f7b2d6db 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +Search API 1.x, dev (xxxx-xx-xx): +--------------------------------- + Search API 1.24 (2018-04-05): ----------------------------- - #2958201 by jcnventura, drunken monkey: Reverted issue #2566529: Added From 5f46652224fc63440a263825c7f0f5bbbccab1e8 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Sat, 14 Apr 2018 15:17:50 +0200 Subject: [PATCH 274/278] Issue #2949899 by drunken monkey, DamienMcKenna: Added a warning against using particular processors with Solr servers to the "Workflow" tab. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f7b2d6db..abefc7e9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2949899 by drunken monkey, DamienMcKenna: Added a warning against using + particular processors with Solr servers to the "Workflow" tab. Search API 1.24 (2018-04-05): ----------------------------- diff --git a/search_api.admin.inc b/search_api.admin.inc index 3afbaa78..1575a14b 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1566,10 +1566,13 @@ function search_api_admin_index_workflow(array $form, array &$form_state, Search $form['processors'] = array( '#type' => 'fieldset', '#title' => t('Processors'), - '#description' => t('Select processors which will pre- and post-process data at index and search time, and their order. ' . - 'Most processors will only influence fulltext fields, but refer to their individual descriptions for details regarding their effect.'), + '#description' => '

    ' . t("Select processors which will pre- and post-process data at index and search time, and their order. Most processors will only influence fulltext fields, but refer to their individual descriptions for details regarding their effect.
    Also, some processors shouldn't be used with more advanced search engines (like Solr or Elasticsearch), since the search engine already provides this functionality.") . '

    ', '#collapsible' => TRUE, ); + if ($index->server) { + $form['processors']['#description'] .= '

    ' . t("Check the server's service class description for details.", + array('@server-url' => url('admin/config/search/search_api/server/' . $index->server . '/edit'))) . '

    '; + } // Processor status. $form['processors']['status'] = array( @@ -1822,8 +1825,8 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp 'In any case, fields of type "Fulltext" will always be fulltext-searchable.

    '), ); if ($index->server) { - $form['description']['#description'] .= '

    ' . t('Check the ' . "server's service class description for details.", - array('@server-url' => url('admin/config/search/search_api/server/' . $index->server))) . '

    '; + $form['description']['#description'] .= '

    ' . t("Check the server's service class description for details.", + array('@server-url' => url('admin/config/search/search_api/server/' . $index->server . '/edit'))) . '

    '; } foreach ($fields as $key => $info) { $form['fields'][$key]['title']['#markup'] = check_plain($info['name']); From 52d22453094f18bc1a9f334f59e0a208707bff24 Mon Sep 17 00:00:00 2001 From: git Date: Sun, 22 Apr 2018 19:31:34 +0200 Subject: [PATCH 275/278] Issue #2828883 by JorgenSandstrom, drunken monkey: Fixed property type for string-typed aggregated fields. --- CHANGELOG.txt | 2 ++ includes/callback_add_aggregation.inc | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index abefc7e9..9205495b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2828883 by JorgenSandstrom, drunken monkey: Fixed property type for + string-typed aggregated fields. - #2949899 by drunken monkey, DamienMcKenna: Added a warning against using particular processors with Solr servers to the "Workflow" tab. diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc index 2e744f60..55ed611c 100644 --- a/includes/callback_add_aggregation.inc +++ b/includes/callback_add_aggregation.inc @@ -328,10 +328,10 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'count' => 'integer', 'max' => 'integer', 'min' => 'integer', - 'first' => 'string', - 'first_char' => 'string', - 'last' => 'string', - 'list' => 'list', + 'first' => 'token', + 'first_char' => 'token', + 'last' => 'token', + 'list' => 'list', ); case 'description': return array( From e89928381910ed7e996bbbb35cf6df410f545930 Mon Sep 17 00:00:00 2001 From: git Date: Sun, 22 Apr 2018 22:23:03 +0200 Subject: [PATCH 276/278] Issue Issue #2948820 by capysara, drunken monkey: Added a link to the "need to reindex" message on the Filters tab. --- CHANGELOG.txt | 2 ++ search_api.admin.inc | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9205495b..8d3804a8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- Issue #2948820 by capysara, drunken monkey: Added a link to the "need to + reindex" message on the Filters tab. - #2828883 by JorgenSandstrom, drunken monkey: Fixed property type for string-typed aggregated fields. - #2949899 by drunken monkey, DamienMcKenna: Added a warning against using diff --git a/search_api.admin.inc b/search_api.admin.inc index 1575a14b..c1b1930c 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1699,6 +1699,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) unset($values['callbacks']['settings']); unset($values['processors']['settings']); $index = $form_state['index']; + $index_path = 'admin/config/search/search_api/index/' . $index->machine_name; $options = empty($index->options) ? array() : $index->options; @@ -1764,13 +1765,14 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state) $index->save(); $index->reindex(); - drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect.")); + $vars = array('@url' => url($index_path)); + drupal_set_message(t('The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect.', $vars)); } else { drupal_set_message(t('No values were changed.')); } - $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow'; + $form_state['redirect'] = $index_path . '/workflow'; } /** From 8af74952bcc7c8b18944c63fbbfc0e518fae1814 Mon Sep 17 00:00:00 2001 From: Thomas Seidl Date: Fri, 22 Jun 2018 21:09:54 +0200 Subject: [PATCH 277/278] Issue #2408727 by drunken monkey, OliverColeman: Fixed out-of-memory errors when executing pending tasks. --- CHANGELOG.txt | 2 ++ search_api.module | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8d3804a8..3ab7c838 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #2408727 by drunken monkey, OliverColeman: Fixed out-of-memory errors when + executing pending tasks. - Issue #2948820 by capysara, drunken monkey: Added a link to the "need to reindex" message on the Filters tab. - #2828883 by JorgenSandstrom, drunken monkey: Fixed property type for diff --git a/search_api.module b/search_api.module index 8fe3e2a7..4285193e 100644 --- a/search_api.module +++ b/search_api.module @@ -1409,6 +1409,10 @@ function search_api_server_tasks_check(SearchApiServer $server = NULL) { // Sometimes the order of tasks might be important, so make sure to order by // the task ID (which should be in order of insertion). $select->orderBy('t.id'); + // Only retrieve and execute 100 tasks at once, to avoid running out of memory + // or time. We just can't do anything else until all tasks have been resolved, + // but at least we shouldn't crash sites, or keep piling up tasks, that way. + $select->range(0, 100); $tasks = $select->execute(); $executed_tasks = array(); @@ -1465,7 +1469,8 @@ function search_api_server_tasks_check(SearchApiServer $server = NULL) { if (!$executed_tasks) { return TRUE; } - // Otherwise, delete the executed tasks and check if new tasks were created. + // Otherwise, delete the executed tasks and check if new tasks were created + // (or if we didn't even fetch all due to the 100 tasks limit). search_api_server_tasks_delete($executed_tasks); return $count_query->execute()->fetchField() === 0; } From 76ed48cdcd898c761a15e718ea735119193fa2c3 Mon Sep 17 00:00:00 2001 From: daspeter Date: Sat, 14 Jul 2018 11:47:27 +0200 Subject: [PATCH 278/278] Issue #1783746 by das-peter, sammys, SpadXIII, drunken monkey, ruloweb, KarlShea, heshanlk, Anas_maw, pinkonomy, Damien Tournoud, rudiedirkx: Added support for the "(not) between" operator. --- CHANGELOG.txt | 3 + contrib/search_api_views/README.txt | 15 ++ .../includes/handler_filter_date.inc | 47 +++- .../includes/handler_filter_numeric.inc | 209 ++++++++++++++++++ .../includes/handler_filter_options.inc | 1 + .../search_api_views/search_api_views.info | 1 + .../search_api_views.views.inc | 3 + 7 files changed, 272 insertions(+), 7 deletions(-) create mode 100644 contrib/search_api_views/includes/handler_filter_numeric.inc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3ab7c838..1a03e31a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,8 @@ Search API 1.x, dev (xxxx-xx-xx): --------------------------------- +- #1783746 by das-peter, sammys, SpadXIII, drunken monkey, ruloweb, KarlShea, + heshanlk, Anas_maw, pinkonomy, Damien Tournoud, rudiedirkx: Added support + for the "(not) between" operator. - #2408727 by drunken monkey, OliverColeman: Fixed out-of-memory errors when executing pending tasks. - Issue #2948820 by capysara, drunken monkey: Added a link to the "need to diff --git a/contrib/search_api_views/README.txt b/contrib/search_api_views/README.txt index b36a7b55..50cfce1c 100644 --- a/contrib/search_api_views/README.txt +++ b/contrib/search_api_views/README.txt @@ -40,6 +40,21 @@ in that position. If the query is sorted in this way, then the random sort, as an associative array with any of the following keys: - seed: A numeric seed value to use for the random sort. +"BETWEEN operator" feature +-------------------------- +This module defines the "BETWEEN operator" feature (feature key: +"search_api_between") that adds the "BETWEEN" and "NOT BETWEEN" filter +operators to search queries. If your search server supports this feature, you +can use the "Is between" and "Is not between" operators when adding Views +filters for numeric, string or date types. + +For developers: +A service class that wants to support this feature has to accept "BETWEEN" and +"NOT BETWEEN" as additional $operator values in query conditions. The value in +both cases is an array with the keys 0 and 1, with the value under key 0 being +the lower and the value under key 1 being the upper bound for the range in which +the field's value should ("BETWEEN") or should not ("NOT BETWEEN") be. + "Facets block" display ---------------------- Most features should be clear to users of Views. However, the module also diff --git a/contrib/search_api_views/includes/handler_filter_date.inc b/contrib/search_api_views/includes/handler_filter_date.inc index c7897245..1259aa0b 100644 --- a/contrib/search_api_views/includes/handler_filter_date.inc +++ b/contrib/search_api_views/includes/handler_filter_date.inc @@ -6,9 +6,9 @@ */ /** - * Views filter handler base class for handling all "normal" cases. + * Views filter handler base class for handling date fields. */ -class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { +class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilterNumeric { /** * Add a "widget type" option. @@ -88,9 +88,22 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { public function value_form(&$form, &$form_state) { parent::value_form($form, $form_state); + $is_date_popup = ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')); + + // If the operator is between + if ($this->operator == 'between') { + if ($is_date_popup) { + $form['value']['min']['#type'] = 'date_popup'; + $form['value']['min']['#date_format'] = $this->options['date_popup_format']; + $form['value']['min']['#date_year_range'] = $this->options['year_range']; + $form['value']['max']['#type'] = 'date_popup'; + $form['value']['max']['#date_format'] = $this->options['date_popup_format']; + $form['value']['max']['#date_year_range'] = $this->options['year_range']; + } + } // If we are using the date popup widget, overwrite the settings of the form // according to what date_popup expects. - if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) { + elseif ($is_date_popup) { $form['value']['#type'] = 'date_popup'; $form['value']['#date_format'] = $this->options['date_popup_format']; $form['value']['#date_year_range'] = $this->options['year_range']; @@ -115,11 +128,31 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { elseif ($this->operator === 'not empty') { $this->query->condition($this->real_field, NULL, '<>', $this->options['group']); } - else { - while (is_array($this->value)) { - $this->value = $this->value ? reset($this->value) : NULL; + elseif (in_array($this->operator, array('between', 'not between'), TRUE)) { + $min = isset($this->value[0]['min']) ? $this->value[0]['min'] : ''; + if ($min !== '') { + $min = is_numeric($min) ? $min : strtotime($min, REQUEST_TIME); + } + $max = isset($this->value[0]['max']) ? $this->value[0]['max'] : ''; + if ($max !== '') { + $max = is_numeric($max) ? $max : strtotime($max, REQUEST_TIME); } - $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME); + + if (is_numeric($min) && is_numeric($max)) { + $this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']); + } + elseif (is_numeric($min)) { + $operator = $this->operator === 'between' ? '>=' : '<'; + $this->query->condition($this->real_field, $min, $operator, $this->options['group']); + } + elseif (is_numeric($max)) { + $operator = $this->operator === 'between' ? '<=' : '>'; + $this->query->condition($this->real_field, $min, $operator, $this->options['group']); + } + } + else { + $value = isset($this->value[0]) ? $this->value[0]['value'] : $this->value['value']; + $v = is_numeric($value) ? $value : strtotime($value, REQUEST_TIME); if ($v !== FALSE) { $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']); } diff --git a/contrib/search_api_views/includes/handler_filter_numeric.inc b/contrib/search_api_views/includes/handler_filter_numeric.inc new file mode 100644 index 00000000..398b2e5b --- /dev/null +++ b/contrib/search_api_views/includes/handler_filter_numeric.inc @@ -0,0 +1,209 @@ + array( + 'value' => array('default' => ''), + 'min' => array('default' => ''), + 'max' => array('default' => ''), + ), + ); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function operator_options() { + $operators = parent::operator_options(); + + $index = search_api_index_load(substr($this->table, 17)); + $server = NULL; + try { + if ($index) { + $server = $index->server(); + } + } + catch (SearchApiException $e) { + // Ignore. + } + if ($server && $server->supportsFeature('search_api_between')) { + $operators += array( + 'between' => t('Is between'), + 'not between' => t('Is not between'), + ); + } + + return $operators; + } + + /** + * Provides a form for setting the filter value. + * + * Heavily borrowed from views_handler_filter_numeric. + * + * @see views_handler_filter_numeric::value_form() + */ + public function value_form(&$form, &$form_state) { + $form['value']['#tree'] = TRUE; + + $single_field_operators = $this->operator_options(); + unset($single_field_operators['empty'], $single_field_operators['not empty'], $single_field_operators['between']); + + // We have to make some choices when creating this as an exposed + // filter form. For example, if the operator is locked and thus + // not rendered, we can't render dependencies; instead we only + // render the form items we need. + $which = 'all'; + if (!empty($form['operator'])) { + $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator'; + } + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // Exposed and locked. + $which = ($this->operator == 'between') ? 'minmax' : 'value'; + } + else { + $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']); + } + } + + // Hide the value box if the operator is 'empty' or 'not empty'. + // Radios share the same selector so we have to add some dummy selector. + if ($which == 'all') { + $form['value']['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => $this->value['value'], + '#dependency' => array($source => array_keys($single_field_operators)), + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['value'])) { + $form_state['input'][$identifier]['value'] = $this->value['value']; + } + } + elseif ($which == 'value') { + // When exposed we drop the value-value and just do value if + // the operator is locked. + $form['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => isset($this->value['value']) ? $this->value['value'] : '', + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = isset($this->value['value']) ? $this->value['value'] : ''; + } + } + + if ($which == 'all' || $which == 'minmax') { + $form['value']['min'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Min') : '', + '#size' => 30, + '#default_value' => $this->value['min'], + ); + $form['value']['max'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('And max') : t('And'), + '#size' => 30, + '#default_value' => $this->value['max'], + ); + + if ($which == 'all') { + $form['value']['min']['#dependency'] = array($source => array('between')); + $form['value']['max']['#dependency'] = array($source => array('between')); + } + + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) { + $form_state['input'][$identifier]['min'] = $this->value['min']; + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) { + $form_state['input'][$identifier]['max'] = $this->value['max']; + } + + if (!isset($form['value']['value'])) { + // Ensure there is something in the 'value'. + $form['value']['value'] = array( + '#type' => 'value', + '#value' => NULL, + ); + } + } + } + + /** + * {@inheritdoc} + */ + public function admin_summary() { + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + + if ($this->operator === 'empty') { + return t('is empty'); + } + if ($this->operator === 'not empty') { + return t('is not empty'); + } + + $value = isset($this->value[0]) ? $this->value[0] : $this->value; + + if (in_array($this->operator, array('between', 'not between'), TRUE)) { + // This is of course wrong for translation purposes, but copied from + // views_handler_filter_numeric::admin_summary() so probably still better + // to re-use this than to do it correctly. + $operator = $this->operator === 'between' ? t('between') : t('not between'); + $vars = array( + '@min' => (string) $value['min'], + '@max' => (string) $value['max'], + ); + return $operator . ' ' . t('@min and @max', $vars); + } + + return check_plain((string) $this->operator) . ' ' . check_plain((string) $value['value']); + + } + + /** + * {@inheritdoc} + */ + public function query() { + if (in_array($this->operator, array('between', 'not between'), TRUE)) { + $min = isset($this->value[0]['min']) ? $this->value[0]['min'] : ''; + $max = isset($this->value[0]['max']) ? $this->value[0]['max'] : ''; + if ($min !== '' && $max !== '') { + $this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']); + } + elseif ($min !== '') { + $operator = $this->operator === 'between' ? '>=' : '<'; + $this->query->condition($this->real_field, $min, $operator, $this->options['group']); + } + elseif ($max !== '') { + $operator = $this->operator === 'between' ? '<=' : '>'; + $this->query->condition($this->real_field, $min, $operator, $this->options['group']); + } + } + else { + parent::query(); + } + } + +} diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc index 3040fb0f..2184fc85 100644 --- a/contrib/search_api_views/includes/handler_filter_options.inc +++ b/contrib/search_api_views/includes/handler_filter_options.inc @@ -121,6 +121,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { */ public function option_definition() { $options = parent::option_definition(); + $options['value'] = array('default' => ''); $options['expose']['contains']['reduce'] = array('default' => FALSE); return $options; } diff --git a/contrib/search_api_views/search_api_views.info b/contrib/search_api_views/search_api_views.info index d9d7ea5f..9f2655ee 100644 --- a/contrib/search_api_views/search_api_views.info +++ b/contrib/search_api_views/search_api_views.info @@ -19,6 +19,7 @@ files[] = includes/handler_filter_date.inc files[] = includes/handler_filter_entity.inc files[] = includes/handler_filter_fulltext.inc files[] = includes/handler_filter_language.inc +files[] = includes/handler_filter_numeric.inc files[] = includes/handler_filter_options.inc files[] = includes/handler_filter_taxonomy_term.inc files[] = includes/handler_filter_text.inc diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index ff52d692..5982465b 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -219,6 +219,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $table[$id]['filter']['vocabulary'] = $vocabulary; } } + elseif (in_array($inner_type, array('integer', 'decimal', 'duration', 'string'))) { + $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterNumeric'; + } else { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter'; }