diff --git a/1 b/1 new file mode 100644 index 0000000..e69de29 diff --git a/sites/all/libraries/plupload/examples/bg.jpg b/sites/all/libraries/plupload/examples/bg.jpg deleted file mode 100644 index 9b27383..0000000 Binary files a/sites/all/libraries/plupload/examples/bg.jpg and /dev/null differ diff --git a/sites/all/libraries/plupload/examples/custom.html b/sites/all/libraries/plupload/examples/custom.html deleted file mode 100644 index e5411ae..0000000 --- a/sites/all/libraries/plupload/examples/custom.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - -Plupload - Queue widget example - - - - - - - - - - - - - - - - - - -
-

Custom example

- -

Shows you how to use the core plupload API.

- -
-
No runtime found.
-
- [Select files] - [Upload files] -
- - -
- - - - \ No newline at end of file diff --git a/sites/all/libraries/plupload/examples/dump.php b/sites/all/libraries/plupload/examples/dump.php deleted file mode 100644 index e4b42e4..0000000 --- a/sites/all/libraries/plupload/examples/dump.php +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -Plupload - Form dump - - - - -

Post dump

- -

Shows the form items posted.

- - - - - - - $value) { ?> - - - - - -
NameValue
- - - diff --git a/sites/all/libraries/plupload/examples/jquery/events.html b/sites/all/libraries/plupload/examples/jquery/events.html deleted file mode 100644 index 263e814..0000000 --- a/sites/all/libraries/plupload/examples/jquery/events.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - -Plupload - Events example - - - - - - - - - - - - - - - - - -
-

Events example

- -

Shows how to bind and use all available events.

- -

Log messages

- - -

Queue widget

-
You browser doesn't support upload.
- Clear queue -
- - - \ No newline at end of file diff --git a/sites/all/libraries/plupload/examples/jquery/jquery_ui_widget.html b/sites/all/libraries/plupload/examples/jquery/jquery_ui_widget.html deleted file mode 100644 index b22e5af..0000000 --- a/sites/all/libraries/plupload/examples/jquery/jquery_ui_widget.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - -Plupload - Queue widget example - - - - - - - - - - - - - - - - - - - - - -

jQuery UI Widget

- -

You can see this example with different themes on the www.plupload.com website.

- -
-
-

You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.

-
-
- - - \ No newline at end of file diff --git a/sites/all/libraries/plupload/examples/jquery/queue_widget.html b/sites/all/libraries/plupload/examples/jquery/queue_widget.html deleted file mode 100644 index 569564a..0000000 --- a/sites/all/libraries/plupload/examples/jquery/queue_widget.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - -Plupload - Queue widget example - - - - - - - - - - - - - - - - - - - -
-

Queue widget example

- -

Shows the jQuery Plupload Queue widget and under different runtimes.

- -
-

Flash runtime

-
You browser doesn't have Flash installed.
- -

Gears runtime

-
You browser doesn't have Gears installed.
-
- -
-

Silverlight runtime

-
You browser doesn't have Silverlight installed.
- -

HTML 5 runtime

-
You browser doesn't support native upload. Try Firefox 3 or Safari 4.
-
- -
-

BrowserPlus runtime

-
You browser doesn't have BrowserPlus installed.
- -

HTML 4 runtime

-
You browser doesn't have HTML 4 support.
-
- -
- - -
- - - - - \ No newline at end of file diff --git a/sites/all/libraries/plupload/examples/jquery/s3.php b/sites/all/libraries/plupload/examples/jquery/s3.php deleted file mode 100644 index 6c8be8a..0000000 --- a/sites/all/libraries/plupload/examples/jquery/s3.php +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - -In our tests SilverLight didn't require anything special and worked with this configuration just fine. It may fail back -to the same crossdomain.xml as last resort. - -!!!Important!!! Plupload UI Widget here, is used only for demo purposes and is not required for uploading to S3. -*/ - -// important variables that will be used throughout this example -$bucket = 'BUCKET'; - -// these can be found on your Account page, under Security Credentials > Access Keys -$accessKeyId = 'ACCESS_KEY_ID'; -$secret = 'SECRET_ACCESS_KEY'; - - -// hash_hmac — Generate a keyed hash value using the HMAC method -// (PHP 5 >= 5.1.2, PECL hash >= 1.1) -if (!function_exists('hash_hmac')) : -// based on: http://www.php.net/manual/en/function.sha1.php#39492 -function hash_hmac($algo, $data, $key, $raw_output = false) -{ - $blocksize = 64; - if (strlen($key) > $blocksize) - $key = pack('H*', $algo($key)); - - $key = str_pad($key, $blocksize, chr(0x00)); - $ipad = str_repeat(chr(0x36), $blocksize); - $opad = str_repeat(chr(0x5c), $blocksize); - $hmac = pack('H*', $algo(($key^$opad) . pack('H*', $algo(($key^$ipad) . $data)))); - - return $raw_output ? $hmac : bin2hex($hmac); -} -endif; - -// prepare policy -$policy = base64_encode(json_encode(array( - // ISO 8601 - date('c'); generates uncompatible date, so better do it manually - 'expiration' => date('Y-m-d\TH:i:s.000\Z', strtotime('+1 day')), - 'conditions' => array( - array('bucket' => $bucket), - array('acl' => 'public-read'), - array('starts-with', '$key', ''), - // for demo purposes we are accepting only images - array('starts-with', '$Content-Type', 'image/'), - // "Some versions of the Adobe Flash Player do not properly handle HTTP responses that have an empty body. - // To configure POST to return a response that does not have an empty body, set success_action_status to 201. - // When set, Amazon S3 returns an XML document with a 201 status code." - // http://docs.amazonwebservices.com/AmazonS3/latest/dev/HTTPPOSTFlash.html - array('success_action_status' => '201'), - // Plupload internally adds name field, so we need to mention it here - array('starts-with', '$name', ''), - // One more field to take into account: Filename - gets silently sent by FileReference.upload() in Flash - // http://docs.amazonwebservices.com/AmazonS3/latest/dev/HTTPPOSTFlash.html - array('starts-with', '$Filename', ''), - ) -))); - -// sign policy -$signature = base64_encode(hash_hmac('sha1', $policy, $secret, true)); - -?> - - - - -Plupload to Amazon S3 Example - - - - - - - - - - - - - - - - - - - - - - - -

Plupload to Amazon S3 Example

- -
-

You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.

-
- - - - - \ No newline at end of file diff --git a/sites/all/libraries/plupload/examples/upload.php b/sites/all/libraries/plupload/examples/upload.php deleted file mode 100644 index 6cf7f85..0000000 --- a/sites/all/libraries/plupload/examples/upload.php +++ /dev/null @@ -1,124 +0,0 @@ - \ No newline at end of file diff --git a/sites/all/modules/contrib/search/search_api/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api/CHANGELOG.txt index 1595674..c1e66d9 100644 --- a/sites/all/modules/contrib/search/search_api/CHANGELOG.txt +++ b/sites/all/modules/contrib/search/search_api/CHANGELOG.txt @@ -1,7 +1,494 @@ -Search API 1.x, dev (xx/xx/xxxx): ---------------------------------- +Search API 1.26 (2019-03-11): +----------------------------- +- #2324023 by drumm, drunken monkey: Changed Views field definition for to + float. +- #3008849 by pamatt, drunken monkey: Fixed non-exposed numeric and date + filters in Views. +- #3009744 by evgeny.chernyavskiy, drunken monkey: Fixed wrong "continue" in + search_api_server_tasks_check(). +- #3003742 by Jelle_S, drunken monkey: Fixed problems with Views date filters. +- #3002043 by alonaoneill, drunken monkey: Fixed module name capitalization and + dependency namespacing in .info files. +- #2990940 by drunken monkey: Fixed multi-byte handling of Highlight processor. +- #3001424 by drunken monkey: Fixed notice when configuring the More Like This + contextual filter. -Search API 1.4 (01/09/2013): +Search API 1.25 (2018-09-17): +----------------------------- +- #2408727 by swim, drunken monkey: Added a batch operation for executing + pending tasks. +- #2325917 by guillaumev, drunken monkey: Added a Views cache plugin based on + Views Content Cache. +- #2989578 by KarlShea, drunken monkey: Fixed Views exposed form fields for + "not between" operator. +- #2982167 by osopolar, drunken monkey: Added a Drush command for re-indexing + specific entities. +- #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 + 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 + particular processors with Solr servers to the "Workflow" tab. + +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. + +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 + 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 + 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 + 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 + handler. +- #2904268 by pobster, drunken monkey: Added support for language hierarchy in + Views. + +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 + "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 + 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. +- #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): +----------------------------- +- #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" + 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. +- #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. +- #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. +- #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. +- #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 + 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. +- #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. + +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 + paths support to the Views facets block. +- #2753441 by Johnny vd Laar: Fixed translated field names in + language-independent cache. + +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 + 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 + 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 + 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 + 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 + 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 + field values sometimes. + +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): +----------------------------- +- #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 + 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. +- #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 + 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. +- #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 + 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". +- #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 + 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. +- #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" + options +- #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): +----------------------------- +- #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, + 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. +- #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 +- #2447213 by drunken monkey: Fixed issues with stale field settings for MLT + contextual filter. + +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. +- #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. +- #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. +- #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 (2014-12-26): +----------------------------- +- #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. +- #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(). +- #2364875 by Xano: Fixed Views argument handler for fulltext fields. +- #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 + 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 + 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. + +Search API 1.13 (2014-07-23): +----------------------------- +- #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 + word. +- #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. + +Search API 1.12 (2014-05-23): +----------------------------- +- #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. +- #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. +- #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. +- #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. +- #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. +- #1227702 by drunken monkey: Improved error handling. + +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 + "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. +- #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. +- #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 (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. +- #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. +- #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 + 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. +- #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. +- #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. +- #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". +- #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 + 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 + API landing page. + +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. +- #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 + dates. +- #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. +- #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 + callbacks. +- #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. +- #2084953 by Yaron Tal: Fixed issue with theme initialization. +- #2075839 by leeomara, drunken monkey: Added descriptions to field lists for + 'Aggregated Fields'. + +Search API 1.8 (2013-09-01): +---------------------------- +- #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. +- #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. +- #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 + selection for checkboxes. +- #1414078 by drunken monkey, jaxxed: Fixed revert of exportables. +- #2011396 by drunken monkey: Fixed support for several facets on a single + field. +- #2050117 by izus, drunken monkey: Updated README.txt to reflect removed + sub-modules. +- #2041365 by drunken monkey: Fixed error reporting for the MLT contextual + filter. +- #2044711 by stBorchert, drunken monkey: Fixed facet adapter's + getCurrentSearch() method to not cache failed attempts. +- #1411712 by Krasnyj, drunken monkey: Fixed notices in Views with groups. +- #1959506 by jantoine, drunken monkey: Fixed "search id" for Views facets + block display. +- #1902168 by rbruhn, drunken monkey, mpv: Fixed fatal error during Features + import. +- #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 (2013-07-01): +---------------------------- +- #1612708 by drunken monkey: Fixed Views caching with facet blocks. +- #2024189 by drunken monkey: Improved serialization of the query class. +- #1311260 by drunken monkey: Fixed tokenizing of string fields. +- #1246998 by drunken monkey: Fixed deletion of items in read-only indexes. +- #1310970 by drunken monkey: Added improved UI help for determining which + fields are available for sorting. +- #1886738 by chx, Jelle_S, drunken monkey: Added Role filter data alteration. +- #1837782 by drunken monkey: Fixed enabling of indexes through the Status tab. +- #1382170 by orakili, lliss, drunken monkey: Added OR filtering for Views + option filter. +- #2012706 by drunken monkey: Fixed $reset parameter for load functions. +- #1851204 by mvc: Fixed exception when indexing book hierarchy. +- #1926030 by stella: Added field machine name to indexes' "Fields" tabs. +- #1879102 by fearlsgroove: Fixed Drush attempting to index 0 items. +- #1999858 by drunken monkey: Cleaned up API documentation for data alterations. +- #2010116 by drunken monkey: Enabled "Index items immediately" for the default + node index. +- #2013581 by drunken monkey: Added multi-valued field to test module. +- #1288724 by brunodbo, drunken monkey, fearlsgroove: Added option for using OR + in Views fulltext search. +- #1694832 by drunken monkey: Fixed index field settings getting stale when + Field API field settings change. +- #1285794 by drunken monkey: Fixed "All" option in Views' exposed "Items per + page" setting. + +Search API 1.6 (2013-05-29): +---------------------------- +- #1649976 by Berdir, ilari.stenroth, drunken monkey: Fixed memory error during + crons run for large indexes. +- #1346276 by drunken monkey: Fixed Tokenizer should only run on fulltext + fields. +- #1697246 by drunken monkey: Added 'Parse mode' option to views. +- #1993536 by drunken monkey, jpieck: Fixed handling of empty values in + processors. +- #1992228 by drunken monkey: Fixed current search block for empty keys. +- #1696434 by orakili, ldweeks, drunken monkey: Added Views argument handler for + all indexed taxonomy term fields. +- #1988238 by esbenvb, drunken monkey: Fixed Views result display for deleted + entities. +- #872912 by drunken monkey: Expanded and fixed test cases. +- #1760706 by jgraham, das-peter, drunken monkey: Added a flexible way for + determining whether an index contains entities. + +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. +- #1792296 by andrewbelcher, drunken monkey: Added a group for Search API hooks. +- #1407844 by nbucknor: Added "exclude" option for Views contextual filters. +- #1278942 by Simon Georges, drunken monkey: Added an option to apply + entity_access() to Views results. +- #1819412 by drunken monkey: Added clean way for retrieving an index's data + alterations and processors. +- #1838134 by das-peter, drunken monkey: Added hook_search_api_items_indexed(). +- #1471310 by drunken monkey: Fixed handling of unset fields when indexing. +- #1944394 by drunken monkey: Added caching to SearchApiIndex::getFields(). +- #1594762 by drunken monkey, alanom, esclapes: Fixed detection of deleted items + in the Hierarchy data alteration. +- #1702604 by JvE, slucero: Added option for maximum date facet depth. + +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. @@ -11,7 +498,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 @@ -27,7 +514,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 @@ -40,7 +527,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. @@ -51,7 +538,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. @@ -69,7 +556,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. @@ -140,7 +627,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. @@ -154,7 +641,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 @@ -196,7 +683,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. @@ -215,12 +702,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. @@ -263,7 +750,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. @@ -285,7 +772,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. @@ -327,7 +814,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: diff --git a/sites/all/modules/contrib/search/search_api/README.txt b/sites/all/modules/contrib/search/search_api/README.txt index a05bfa3..a6264e9 100644 --- a/sites/all/modules/contrib/search/search_api/README.txt +++ b/sites/all/modules/contrib/search/search_api/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 @@ -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 @@ -105,8 +105,10 @@ IMPORTANT: Access checks specific search types, if available. As stated above, you will need at least one other module to use the Search API, -namely one that defines a service class (e.g. search_api_db ("Database search"), -provided with this module). +namely one that defines a service class (e.g., search_api_db ("Database search") +which can be found at [3]). + +[3] http://drupal.org/project/search_api_db - Creating a server (Configuration > Search API > Add server) @@ -169,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 @@ -221,9 +223,9 @@ Information for developers | For custom field types to be available for indexing, provide a | "property_type" key in hook_field_info(), and optionally a callback at the | "property_callbacks" key. - | Both processes are explained in [1]. + | Both processes are explained in [4]. | - | [1] http://drupal.org/node/1021466 + | [4] http://drupal.org/node/1021466 Apart from improving the module itself, developers can extend search capabilities provided by the Search API by providing implementations for one (or @@ -231,7 +233,9 @@ several) of the following classes. Detailed documentation on the methods that need to be implemented are always available as doc comments in the respective interface definition (all found in their respective files in the includes/ directory). The details for hooks can be looked up in the search_api.api.php -file. +file. Note that all hooks provided by the Search API use the "search_api" hook +group. Therefore, implementations of the hook can be moved into a +MODULE.search_api.inc file in your module's directory. For all interfaces there are handy base classes which can (but don't need to) be used to ease custom implementations, since they provide sensible generic implementations for many methods. They, too, should be documented well enough @@ -255,7 +259,9 @@ service class. The central methods here are the indexItems() and the search() methods, which always have to be overridden manually. The configurationForm() method allows services to provide custom settings for the user. -See the SearchApiDbService class for an example implementation. +See the SearchApiDbService class provided by [5] for an example implementation. + +[5] http://drupal.org/project/search_api_db - Query class Interface: SearchApiQueryInterface @@ -334,15 +340,6 @@ See the processors in includes/processor.inc for examples. Included components ------------------- -- Service classes - - * Database search - A search server implementation that uses the normal database for indexing - data. It isn't very fast and the results might also be less accurate than - with third-party solutions like Solr, but it's very easy to set up and good - for smaller applications or testing. - See contrib/search_api_db/README.txt for details. - - Data alterations * URL field @@ -388,17 +385,19 @@ 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 - * Search pages - This module lets you create simple search pages for indexes. * Search views - This integrates the Search API with the Views module [1], enabling the user + This integrates the Search API with the Views module [6], enabling the user to create views which display search results from any Search API index. * Search facets For service classes supporting this feature (e.g. Solr search), this module automatically provides configurable facet blocks on pages that execute a search query. -[1] http://drupal.org/project/views +[6] http://drupal.org/project/views diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc index ee27201..a5b5cdc 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc +++ b/sites/all/modules/contrib/search/search_api/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(); @@ -78,21 +82,21 @@ 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. + // 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'], @@ -103,13 +107,35 @@ 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. */ 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; } } @@ -128,8 +154,10 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { * search_api_current_search(). Or NULL, if no match was found. */ public function getCurrentSearch() { + // Even if this fails once, there might be a search query later in the page + // request. We therefore don't store anything in $this->current_search in + // case of failure, but just try again if the method is called again. if (!isset($this->current_search)) { - $this->current_search = FALSE; $index_id = $this->info['instance']; // There is currently no way to configure the "current search" block to // show on a per-searcher basis as we do with the facets. Therefore we @@ -137,13 +165,13 @@ 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; } } } - return $this->current_search ? $this->current_search : NULL; + return $this->current_search; } /** @@ -164,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 @@ -172,16 +206,6 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { // properly. $keys = '[' . t('complex query') . ']'; } - elseif (!$keys) { - // If a base path other than the current one is set, we assume that we - // shouldn't report on the current search. Highly hack-y, of course. - if ($search[0]->getOption('search_api_base_path', $_GET['q']) !== $_GET['q']) { - return NULL; - } - // Work-around since Facet API won't show the "Current search" block - // without keys. - $keys = '[' . t('all items') . ']'; - } drupal_alter('search_api_facetapi_keys', $keys, $search[0]); return $keys; } @@ -204,7 +228,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()); @@ -213,6 +236,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'), @@ -222,6 +246,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, @@ -238,5 +263,58 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter { '#value' => array(), ); } + + // Add a granularity option to date query types. + if (isset($facet['query type']) && $facet['query type'] == 'date') { + $granularity_options = array( + FACETAPI_DATE_YEAR => t('Years'), + FACETAPI_DATE_MONTH => t('Months'), + FACETAPI_DATE_DAY => t('Days'), + FACETAPI_DATE_HOUR => t('Hours'), + FACETAPI_DATE_MINUTE => t('Minutes'), + FACETAPI_DATE_SECOND => t('Seconds'), + ); + + $form['global']['date_granularity'] = array( + '#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, + ); + + // 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. + 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/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc index 2574ba8..6aeb836 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc +++ b/sites/all/modules/contrib/search/search_api/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']])) { @@ -46,14 +57,145 @@ 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']); - $this->addFacetFilter($query, $field, $filter); + $filter = $this->createRangeFilter($item['value']); + if ($filter) { + $this->addFacetFilter($query, $field, $filter); + } } } + /** + * Rewrites the handler-specific date range syntax to the normal facet syntax. + * + * @param string $value + * The user-facing facet value. + * + * @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) { + // Ignore any filters passed directly from the server (range or missing). + if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ TO [^ ]+[\])]$/', $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; + } + + $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]"; + } + + /** + * 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. + * + * Not used anymore, but kept for backwards compatibility with potential + * subclasses. */ public function replaceDateString($matches) { return strtotime($matches[0]); @@ -68,25 +210,20 @@ 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]; - 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['field']])) { - $values = $results['search_api_facets'][$this->facet['field']]; + 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) { if ($value['count']) { $filter = $value['filter']; @@ -102,33 +239,46 @@ 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']; } } } } + $settings = $facet->getSettings()->settings; + + // 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 = $gap = NULL; - foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) { + $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, minute being the lowest we can go. - 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; } - $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, FACETAPI_DATE_MINUTE); + + $granularity = search_api_facetapi_date_get_granularity($value); + if (!$granularity) { + continue; + } + $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. @@ -140,6 +290,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue // Stores the last value iterated over. $parent = $value; } + if (empty($raw_values)) { return $build; } @@ -149,38 +300,42 @@ 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), $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( + FACETAPI_DATE_YEAR => 6, + FACETAPI_DATE_MONTH => 5, + FACETAPI_DATE_DAY => 4, + FACETAPI_DATE_HOUR => 3, + FACETAPI_DATE_MINUTE => 2, + FACETAPI_DATE_SECOND => 1, + ); + // 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 { - $gap = FACETAPI_DATE_HOUR; + $granularity = $max_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); - } - $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); } - 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; } @@ -193,4 +348,5 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue return $build; } + } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc index 1b2037d..4c674a8 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc @@ -30,53 +30,98 @@ 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->getSettings()->settings; - // Add active facet filters. + // 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; + } + + // Retrieve the active facet filters. $active = $this->adapter->getActiveItems($this->facet); if (empty($active)) { 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. + $operator = $settings['operator']; + if ($operator == FACETAPI_OPERATOR_AND) { + $conjunction = 'AND'; + } + 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 (is_callable($this->facet['hierarchy callback']) && !$settings['flatten']) { + // Check the filters in reverse order, to avoid checking parents that + // will afterwards be removed anyways. + $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; + } + // 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]); + } + } + } + } + } } else { - // Otherwise we set the conditions directly on the query. - $facet_filter = $query; + $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); 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); } /** * Helper method for setting a facet filter on a query or query filter object. */ protected function addFacetFilter($query_filter, $field, $filter) { - // Integer (or other nun-string) filters might mess up some of the following + // Test if this filter should be negated. + $settings = $this->adapter->getFacet($this->facet)->getSettings(); + $exclude = !empty($settings->settings['exclude']); + // Integer (or other non-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 '))) { + 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 == '*') { - $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 +137,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); @@ -113,15 +171,22 @@ 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 = search_api_current_search($search_id); + $search_id = $search_ids[$facet_key]; + list(, $results) = search_api_current_search($search_id); $build = array(); - $results = $search[1]; - if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['field']])) { - $values = $results['search_api_facets'][$this->facet['field']]; + + // Always include the active facet items. + foreach ($this->adapter->getActiveItems($this->facet) as $filter) { + $build[$filter['value']]['#count'] = 0; + } + + // 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) { $filter = $value['filter']; // As Facet API isn't really suited for our native facet filter diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info index ef62e27..904a64d 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info @@ -1,7 +1,7 @@ -name = Search facets +name = Search Facets description = "Integrate the Search API with the Facet API to provide facetted searches." -dependencies[] = search_api -dependencies[] = facetapi +dependencies[] = search_api:search_api +dependencies[] = facetapi:facetapi core = 7.x package = Search @@ -9,9 +9,8 @@ files[] = plugins/facetapi/adapter.inc files[] = plugins/facetapi/query_type_term.inc files[] = plugins/facetapi/query_type_date.inc -; Information added by drupal.org packaging script on 2013-01-09 -version = "7.x-1.4" +; Information added by Drupal.org packaging script on 2019-03-11 +version = "7.x-1.26" core = "7.x" project = "search_api" -datestamp = "1357726719" - +datestamp = "1552334832" diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install index a626a0c..77c0802 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install @@ -5,9 +5,41 @@ * 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 + // 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'); +} + +/** + * 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/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module index b452734..3696eae 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module +++ b/sites/all/modules/contrib/search/search_api/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)), @@ -65,6 +65,9 @@ function search_api_facetapi_facetapi_searcher_info() { 'supports facet mincount' => TRUE, 'include default facets' => FALSE, ); + if (($entity_type = $index->getEntityType()) && $entity_type !== $index->item_type) { + $info[$searcher_name]['types'][] = $entity_type; + } } } return $info; @@ -80,7 +83,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) { if (!empty($index->options['fields'])) { $wrapper = $index->entityWrapper(); $bundle_key = NULL; - if (($entity_info = entity_get_info($index->item_type)) && !empty($entity_info['bundle keys']['bundle'])) { + if ($index->getEntityType() && ($entity_info = entity_get_info($index->getEntityType())) && !empty($entity_info['bundle keys']['bundle'])) { $bundle_key = $entity_info['bundle keys']['bundle']; } @@ -89,12 +92,12 @@ 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', 'map options' => array( - 'map callback' => 'facetapi_map_date', + 'map callback' => 'search_api_facetapi_map_date', ), ), ); @@ -113,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, @@ -144,7 +147,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) { if ($bundle_key) { if ($key === $bundle_key) { // Set entity type this field contains bundle information for. - $facet_info[$key]['field api bundles'][] = $index->item_type; + $facet_info[$key]['field api bundles'][] = $index->getEntityType(); } else { // Add "bundle" as possible dependency plugin. @@ -208,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. */ @@ -215,7 +270,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; @@ -223,6 +278,48 @@ 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. + * + * 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. * @@ -295,13 +392,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"; @@ -313,25 +410,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) { + if ($label !== FALSE) { $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; @@ -342,12 +463,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'); @@ -379,3 +506,126 @@ 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; + } + + // Otherwise, parse the timestamp from the known format and format it as a + // label. + $format = search_api_facetapi_date_get_granularity_format($granularity); + // Use the "!" modifier to make the date parsing independent of the current + // date/time. (See #2678856.) + $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, + FACETAPI_DATE_MONTH, + FACETAPI_DATE_DAY, + FACETAPI_DATE_HOUR, + FACETAPI_DATE_MINUTE, + FACETAPI_DATE_SECOND, + ); + + if (!in_array($precision, $formats)) { + $precision = FACETAPI_DATE_YEAR; + } + return format_date($timestamp, 'search_api_facetapi_' . $precision); +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt index e04a7a3..50cfce1 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt @@ -24,6 +24,37 @@ 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. + +"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 @@ -54,8 +85,7 @@ linked to for the filter to have an effect. Since the block will trigger a search on pages where it is set to appear, you can also enable additional „normal“ facet blocks for that search, via the „Facets“ tab for the index. They will automatically also point to the same -search that you specified for the display. The Search ID of the „Facets blocks“ -display can easily be recognized by the "-facet_block" suffix. +search that you specified for the display. If you want to use only the normal facets and not display anything at all in the Views block, just activate the display's „Hide block“ option. @@ -63,6 +93,50 @@ Note: If you want to display the block not only on a few pages, you should in any case take care that it isn't displayed on the search page, since that might confuse users. +Access features +--------------- +Search views created with this module contain two query settings (located in +the "Advanced" fieldset) which let you control the access checks executed for +search results displayed in the view. + +- Bypass access checks +This option allows you to deactivate access filters that would otherwise be +added to the search, if the index supports this. This is, for instance, the case +for indexes on the "Node" item type, when the "Node access" data alteration is +activated. +Use this either to slightly speed up searches where additional checks are +unnecessary (e.g., because you already filter on "Node: Published") and there is +no other node access mechanism on your site) or to show certain data that users +normally wouldn't have access to (e.g., a list of all matching node titles, +published or not). + +- Additional access checks on result entities +When this option is activated, all result entities will be passed to an +additional access check, even if search-time access checks are available for +this index. The advantage is that access rules are guaranteed to be enforced – +stale data in the index, which might make other access checks incorrect, won't +influence this access check. You can also use it for item types for which no +other access mechanisms are available. +However, note that results filtered out this way will mess up paging, result +counts and possibly other things too (like facet counts), as the result row is +only hidden from display after the search has been executed. Where possible, +you should therefore only use this in combination with appropriate filter +settings ensuring that only when the index isn't up-to-date items will be +filtered out this way. +This option is only available for indexes on entity types. + +Other features +-------------- +- Change parse mode +You can determine how search keys entered by the user will be parsed by going to +"Advanced" > "Query settings" within your View's settings. "Direct" can be +useful, e.g., when you want to give users the full power of Solr. In other +cases, "Multiple terms" is usually what you want / what users expect. +Caution: For letting users use fulltext searches, always use the "Search: +Fulltext search" filter or contextual filter – using a normal filter on a +fulltext field won't parse the search keys, which means multiple words will only +be found when they appear as that exact phrase. + FAQ: Why „*Indexed* Node“? -------------------------- The group name used for the search result itself (in fields, filters, etc.) is diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc index c7b1e3a..00e80c2 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc +++ b/sites/all/modules/contrib/search/search_api/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')) { @@ -177,9 +175,19 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { 'min_count' => 1, ); } - $query_options['search id'] = 'search_api_views:' . $this->view->name . '-facets_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(); @@ -230,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'), @@ -239,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'), @@ -250,10 +283,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; diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc index 123481c..a31738e 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc @@ -1,5 +1,10 @@ FALSE); + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + $options['not'] = array('default' => FALSE, 'bool' => TRUE); return $options; } @@ -79,6 +96,14 @@ class SearchApiViewsHandlerArgument extends views_handler_argument { '#default_value' => $this->options['break_phrase'], '#fieldset' => 'more', ); + + $form['not'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude'), + '#description' => t('If selected, the numbers entered for the filter will be excluded rather than limiting the view.'), + '#default_value' => !empty($this->options['not']), + '#fieldset' => 'more', + ); } /** @@ -86,37 +111,31 @@ class SearchApiViewsHandlerArgument extends views_handler_argument { * * The argument sent may be found at $this->argument. */ - // @todo Provide options to select the operator, instead of always using '='? public function query($group_by = FALSE) { - if (!empty($this->options['break_phrase'])) { - views_break_phrase($this->argument, $this); - } - else { - $this->value = array($this->argument); + if (empty($this->value)) { + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + } } + $operator = empty($this->options['not']) ? '=' : '<>'; + if (count($this->value) > 1) { $filter = $this->query->createFilter(drupal_strtoupper($this->operator)); // $filter will be NULL if there were errors in the query. if ($filter) { foreach ($this->value as $value) { - $filter->condition($this->real_field, $value, '='); + $filter->condition($this->real_field, $value, $operator); } $this->query->filter($filter); } } else { - $this->query->condition($this->real_field, reset($this->value)); + $this->query->condition($this->real_field, reset($this->value), $operator); } } - /** - * Get the title this argument will assign the view, given the argument. - * - * This usually needs to be overridden to provide a proper title. - */ - public function title() { - return t('Search @field for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument)); - } - } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_date.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_date.inc new file mode 100644 index 0000000..a92c896 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_date.inc @@ -0,0 +1,161 @@ +value)) { + $this->fillValue(); + if ($this->value === FALSE) { + $this->abort(); + return; + } + } + + $outer_conjunction = strtoupper($this->operator); + + if (empty($this->options['not'])) { + $operator = '='; + $inner_conjunction = 'OR'; + } + else { + $operator = '<>'; + $inner_conjunction = 'AND'; + } + + 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); + + $inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter); + $range_op = (empty($this->options['not']) ? '>=' : '<'); + $inner_filter->condition($this->real_field, $values[0], $is_range ? $range_op : $operator); + if ($is_range) { + $range_op = (empty($this->options['not']) ? '<=' : '>'); + $inner_filter->condition($this->real_field, $values[1], $range_op); + $value_filter->filter($inner_filter); + } + $outer_filter->filter($value_filter); + } + + $this->query->filter($outer_filter); + } + } + } + + /** + * 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; + } + + return strtotime($value); + } + + /** + * Fills $this->value with data from the argument. + */ + protected function fillValue() { + if (!empty($this->options['break_phrase'])) { + // Set up defaults: + if (!isset($this->value)) { + $this->value = array(); + } + + 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); + } + + // Keep an 'error' value if invalid strings were given. + if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) { + $this->value = FALSE; + } + } + else { + $this->value = array($this->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. + * + * @return string + * A title fitting for the passed 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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc index f53f59cc..a4db8e2 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc @@ -1,9 +1,14 @@ array()); + $options['conjunction'] = array('default' => 'AND'); return $options; } @@ -20,6 +26,8 @@ class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumen public function options_form(&$form, &$form_state) { parent::options_form($form, $form_state); + $form['help']['#markup'] = t('Note: You can change how search keys are parsed under "Advanced" > "Query settings".'); + $fields = $this->getFulltextFields(); if (!empty($fields)) { $form['fields'] = array( @@ -31,6 +39,17 @@ class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumen '#multiple' => TRUE, '#default_value' => $this->options['fields'], ); + $form['conjunction'] = array( + '#title' => t('Operator'), + '#description' => t('Determines how multiple keywords entered for the search will be combined.'), + '#type' => 'radios', + '#options' => array( + 'AND' => t('Contains all of these words'), + 'OR' => t('Contains any of these words'), + ), + '#default_value' => $this->options['conjunction'], + ); + } else { $form['fields'] = array( @@ -47,7 +66,16 @@ 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']); } $old = $this->query->getOriginalKeys(); diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc index b50ab4e..bfb2039 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc @@ -1,5 +1,10 @@ FALSE); $options['fields'] = array('default' => array()); return $options; } @@ -22,8 +27,23 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg public function options_form(&$form, &$form_state) { parent::options_form($form, $form_state); unset($form['break_phrase']); + 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) { @@ -55,23 +75,38 @@ 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); - throw new SearchApiException(t('The search service "@class" does not offer "More like this" functionality.', - array('@class' => $class['name']))); - return; - } - $fields = $this->options['fields'] ? $this->options['fields'] : array(); - if (empty($fields)) { - foreach ($this->query->getIndex()->options['fields'] as $key => $field) { - $fields[] = $key; + 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; } + $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); + } + if ($this->query->getIndex()->datasource() instanceof SearchApiCombinedEntityDataSourceController) { + $id = $this->options['entity_type'] . '/' . $this->argument; + } + else { + $id = $this->argument; + } + + $mlt = array( + 'id' => $id, + '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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_string.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_string.inc new file mode 100644 index 0000000..f932ff0 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_string.inc @@ -0,0 +1,31 @@ +argument. + */ + public function query($group_by = FALSE) { + if (empty($this->value)) { + if (!empty($this->options['break_phrase'])) { + views_break_phrase_string($this->argument, $this); + } + else { + $this->value = array($this->argument); + } + } + + parent::query($group_by); + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc new file mode 100644 index 0000000..3d9fdfc --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc @@ -0,0 +1,104 @@ +argument. + */ + public function query($group_by = FALSE) { + if (empty($this->value)) { + $this->fillValue(); + } + + $outer_conjunction = strtoupper($this->operator); + + if (empty($this->options['not'])) { + $operator = '='; + $inner_conjunction = 'OR'; + } + else { + $operator = '<>'; + $inner_conjunction = 'AND'; + } + + if (!empty($this->value)) { + $terms = entity_load('taxonomy_term', $this->value); + + if (!empty($terms)) { + $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) { + $inner_filter->condition($field, $term->tid, $operator); + } + } + foreach ($vocabulary_fields[''] as $field) { + $inner_filter->condition($field, $term->tid, $operator); + } + if ($outer_conjunction != $inner_conjunction) { + $filter->filter($inner_filter); + } + } + + $this->query->filter($filter); + } + } + } + + /** + * 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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_text.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_text.inc deleted file mode 100644 index cc7ae24..0000000 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_text.inc +++ /dev/null @@ -1,17 +0,0 @@ - $this->definition['title'], '@arg' => $this->argument)); - } - -} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc index 85c6674..770526a 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc @@ -1,5 +1,10 @@ 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'), @@ -46,8 +51,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 +63,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'), + ); + } } /** diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc index b3b2172..2ff1f25 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc @@ -1,5 +1,10 @@ array('default' => 'default'), + 'date_popup_format' => array('default' => 'm/d/Y'), + 'year_range' => array('default' => '-3:+3'), ); } @@ -29,14 +36,49 @@ 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'), '#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')) { + $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')); + } } } @@ -46,11 +88,25 @@ 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'] = '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']); } elseif (empty($form_state['exposed'])) { @@ -66,17 +122,38 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter { * Add this filter to the query. */ public function query() { + $this->normalizeValue(); + 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']); } - 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 = $this->value['min']; + if ($min !== '') { + $min = is_numeric($min) ? $min : strtotime($min, REQUEST_TIME); } - $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME); + $max = $this->value['max']; + if ($max !== '') { + $max = is_numeric($max) ? $max : strtotime($max, 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 { + $v = is_numeric($this->value['value']) ? $this->value['value'] : strtotime($this->value['value'], REQUEST_TIME); if ($v !== FALSE) { $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']); } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc new file mode 100644 index 0000000..ce5c753 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc @@ -0,0 +1,207 @@ + $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 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 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); + } + } + + /** + * {@inheritdoc} + */ + public function value_validate($form, &$form_state) { + if (!empty($form['value'])) { + $value = &$form_state['values']['options']['value']; + 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; + } + } + } + } + + /** + * {@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; + } + + $this->validated_exposed_input = FALSE; + $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 (!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); + } + } + + /** + * 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() { + 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(); + $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)) { + $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), $operator, $this->options['group']); + } + else { + $filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR'); + foreach ($this->value as $value) { + $filter->condition($this->real_field, $value, $operator); + } + $this->query->filter($filter, $this->options['group']); + } + } + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc index b7f2e1e..dcb3e26 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc @@ -1,17 +1,44 @@ operator_form($form, $form_state); + $form['operator']['#description'] = t('This operator is only useful when using \'Search keys\'.'); + } + + /** + * Provide a list of options for the operator form. + */ + public function operator_options() { + return array( + 'AND' => t('Contains all of these words'), + 'OR' => t('Contains any of these words'), + 'NOT' => t('Contains none of these words'), + ); + } + /** * Specify the options this filter uses. */ public function option_definition() { $options = parent::option_definition(); + $options['operator']['default'] = 'AND'; + $options['mode'] = array('default' => 'keys'); + $options['min_length'] = array('default' => ''); $options['fields'] = array('default' => array()); return $options; @@ -27,7 +54,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex '#title' => t('Use as'), '#type' => 'radios', '#options' => array( - 'keys' => t('Search keys – multiple words will be split and the filter will influence relevance.'), + 'keys' => t('Search keys – multiple words will be split and the filter will influence relevance. You can change how search keys are parsed under "Advanced" > "Query settings".'), 'filter' => t("Search filter – use as a single phrase that restricts the result set but doesn't influence relevance."), ), '#default_value' => $this->options['mode'], @@ -54,6 +81,70 @@ 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); + $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; + continue; + } + if ($quoted || $word[0] === '"') { + $quoted = TRUE; + continue; + } + if ($word_length < $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); } /** @@ -68,7 +159,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. @@ -80,17 +172,31 @@ 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; } - $this->query->fields($fields); - $old = $this->query->getOriginalKeys(); + // 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', 'OR'); + } + + 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); - if ($this->operator != '=') { + if ($this->operator == 'NOT') { $keys = &$this->query->getKeys(); if (is_array($keys)) { $keys['#negation'] = TRUE; @@ -99,16 +205,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; + // 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]; + } + } + } } - elseif (is_array($old)) { - // We don't support such nonsense. - } - 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; } } } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc index f95ddaf..3202cdb 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc @@ -14,26 +14,17 @@ 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( - '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; + protected function get_value_options() { + parent::get_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; } /** @@ -41,6 +32,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; @@ -48,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(); } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_numeric.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_numeric.inc new file mode 100644 index 0000000..b4c8ec1 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_numeric.inc @@ -0,0 +1,248 @@ +normalizeValue(); + } + + /** + * {@inheritdoc} + */ + public function option_definition() { + $options = parent::option_definition(); + $options['value'] = array( + 'contains' => 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'], + $single_field_operators['not between'] + ); + $between_operators = array('between', 'not 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'; + $source = NULL; + if (!empty($form['operator'])) { + $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator'; + } + + $identifier = NULL; + 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 = in_array($this->operator, $between_operators) ? '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 ($identifier && !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 ($identifier && !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 => $between_operators); + $form['value']['max']['#dependency'] = array($source => $between_operators); + } + + 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'); + } + + 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) $this->value['min'], + '@max' => (string) $this->value['max'], + ); + return $operator . ' ' . t('@min and @max', $vars); + } + + return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value['value']); + } + + /** + * {@inheritdoc} + */ + public function query() { + $this->normalizeValue(); + + if (in_array($this->operator, array('between', 'not between'), TRUE)) { + $min = $this->value['min']; + $max = $this->value['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 { + // The parent handler doesn't expect our value structure, just pass the + // scalar value instead. + $this->value = $this->value['value']; + parent::query(); + } + } + + /** + * Sets $this->value to an array of options as defined by the filter. + * + * @see SearchApiViewsHandlerFilterNumeric::option_definition() + */ + protected function normalizeValue() { + $value = $this->value; + if (is_array($value) && isset($value[0])) { + $value = $value[0]; + } + if (!is_array($value)) { + $value = array('value' => $value); + } + $this->value = array( + 'value' => isset($value['value']) ? $value['value'] : '', + 'min' => isset($value['min']) ? $value['min'] : '', + 'max' => isset($value['max']) ? $value['max'] : '', + ); + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc index eda4344..2184fc8 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc @@ -1,26 +1,98 @@ 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. */ public function operator_options() { - return array( + $options = array( '=' => t('Is one of'), - '<>' => t('Is not one of'), + 'all of' => t('Is all of'), + '<>' => t('Is none of'), 'empty' => t('Is empty'), 'not empty' => t('Is not empty'), ); + // "Is all of" doesn't make sense for single-valued fields. + if (empty($this->definition['multi-valued'])) { + unset($options['all of']); + } + return $options; } /** @@ -49,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; } @@ -57,13 +130,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; } /** @@ -86,29 +158,38 @@ 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'])), - '#default_value' => isset($this->value) ? $this->value : array(), + '#size' => min(4, count($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. - // #states replace #dependency (http://drupal.org/node/1595022). - $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'), + ); + } } /** @@ -135,19 +216,30 @@ 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]); } } // Choose different kind of ouput for 0, a single and multiple values. if (count($this->value) == 0) { - return $this->operator == '=' ? t('none') : t('any'); + return $this->operator != '<>' ? t('none') : t('any'); } elseif (count($this->value) == 1) { + switch ($this->operator) { + case '=': + case 'all of': + $operator = '='; + break; + + case '<>': + $operator = '<>'; + break; + } // If there is only a single value, use just the plain operator, = or <>. - $operator = check_plain($this->operator); - $values = check_plain($this->definition['options'][reset($this->value)]); + $operator = check_plain($operator); + $values = check_plain($this->value_options[reset($this->value)]); } else { foreach ($this->value as $value) { @@ -158,46 +250,91 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter { $values .= '…'; break; } - $values .= check_plain($this->definition['options'][$value]); + $values .= check_plain($this->value_options[$value]); } } 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. */ public function query() { if ($this->operator === 'empty') { $this->query->condition($this->real_field, NULL, '=', $this->options['group']); + return; } - elseif ($this->operator === 'not empty') { + if ($this->operator === 'not empty') { $this->query->condition($this->real_field, NULL, '<>', $this->options['group']); + return; } - else { - while (is_array($this->value) && count($this->value) == 1) { - $this->value = reset($this->value); + + // Extract the value. + while (is_array($this->value) && count($this->value) == 1) { + $this->value = reset($this->value); + } + + // Determine operator and conjunction. The defaults are already right for + // "all of". + $operator = '='; + $conjunction = 'AND'; + switch ($this->operator) { + case '=': + $conjunction = 'OR'; + break; + + case '<>': + $operator = '<>'; + 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. + if ($this->value === array()) { + if ($operator != '<>') { + $this->query->condition($this->real_field, NULL, '=', $this->options['group']); } - if (is_scalar($this->value) && $this->value !== '') { - $this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']); - } - elseif ($this->value) { - if ($this->operator == '=') { - $filter = $this->query->createFilter('OR'); - // $filter will be NULL if there were errors in the query. - if ($filter) { - foreach ($this->value as $v) { - $filter->condition($this->real_field, $v, '='); - } - $this->query->filter($filter, $this->options['group']); - } - } - else { - foreach ($this->value as $v) { - $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']); - } + return; + } + + if (is_scalar($this->value) && $this->value !== '') { + $this->query->condition($this->real_field, $this->value, $operator, $this->options['group']); + } + elseif ($this->value) { + $filter = $this->query->createFilter($conjunction); + // $filter will be NULL if there were errors in the query. + if ($filter) { + foreach ($this->value as $v) { + $filter->condition($this->real_field, $v, $operator); } + $this->query->filter($filter, $this->options['group']); } } } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc new file mode 100644 index 0000000..51982b0 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc @@ -0,0 +1,335 @@ +definition['vocabulary']); + } + + /** + * {@inheritdoc} + */ + public function option_definition() { + $options = parent::option_definition(); + + $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; + } + + /** + * {@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); + + 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 ($vocabulary && $this->options['type'] == 'textfield') { + $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid; + } + else { + if ($vocabulary && !empty($this->options['hierarchy'])) { + $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) . check_plain(entity_label('taxonomy_term', $term))); + $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('taxonomy_term_access'); + if ($vocabulary) { + $query->condition('tv.machine_name', $vocabulary->machine_name); + } + $result = $query->execute(); + $tids = array(); + + foreach ($result as $term) { + $tids[] = $term->tid; + } + $terms = taxonomy_term_load_multiple($tids); + + foreach ($terms as $term) { + $options[$term->tid] = check_plain(entity_label('taxonomy_term', $term)); + } + } + + $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; + } + + // 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; + } + + 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} + */ + public function expose_options() { + parent::expose_options(); + $this->options['expose']['reduce'] = FALSE; + } + + /** + * {@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); + if (!empty($this->definition['vocabulary'])) { + $query->condition('tv.machine_name', $this->definition['vocabulary']); + } + $query->addTag('taxonomy_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') { + $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'], + ); + } + } + + /** + * {@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', $ids) + ->execute() + ->fetchCol()); + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc index a14d4a4..83152a1 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc @@ -1,5 +1,10 @@ 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; + } + } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc new file mode 100644 index 0000000..6255274 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc @@ -0,0 +1,79 @@ +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); + 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')); + } + 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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc index 463e655..d7ca1e4 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc @@ -1,5 +1,10 @@ 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 + // 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; + } + + try { + $this->query->sort($this->real_field, $this->options['order']); + } + catch (SearchApiException $e) { + $this->query->abort($e->getMessage()); } - $this->query->sort($this->real_field, $this->options['order']); } } diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc new file mode 100644 index 0000000..c63aed5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc @@ -0,0 +1,146 @@ +get_results_key(); + $results = NULL; + $query_plugin = $this->view->query; + if ($query_plugin instanceof SearchApiViewsQuery) { + $results = $query_plugin->getSearchApiResults(); + } + $data = array( + 'result' => $this->view->result, + 'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0, + 'current_page' => $this->view->get_current_page(), + 'search_api results' => $results, + ); + cache_set($cid, $data, $this->table, $this->cache_set_expire($type)); + } + + /** + * Overrides views_plugin_cache::cache_get(). + * + * Additionally stores successfully retrieved results with + * search_api_current_search(). + */ + public function cache_get($type) { + if ($type != 'results') { + return parent::cache_get($type); + } + + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->current_page. + if ($cache = cache_get($this->get_results_key(), $this->table)) { + $cutoff = $this->cache_expire($type); + if (!$cutoff || $cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->set_current_page($cache->data['current_page']); + $this->view->execute_time = 0; + + // Trick Search API into believing a search happened, to make facetting + // et al. work. + $query = $this->getSearchApiQuery(); + search_api_current_search($query->getOption('search id'), $query, $cache->data['search_api results']); + + return TRUE; + } + } + return FALSE; + } + + /** + * Overrides views_plugin_cache::get_cache_key(). + * + * Use the Search API query as the main source for the key. Note that in + * Views < 3.8, this method does not exist. + */ + public function get_cache_key($key_data = array()) { + global $user; + + if (!isset($this->_results_key)) { + $query = $this->getSearchApiQuery(); + $query->preExecute(); + $key_data += array( + 'query' => $query, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'language' => $GLOBALS['language']->language, + 'base_url' => $GLOBALS['base_url'], + '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 + // Search API query object we use. + if (isset($_GET['exposed_info'])) { + $key_data['exposed_info'] = $_GET['exposed_info']; + } + } + $key = drupal_hash_base64(serialize($key_data)); + return $key; + } + + /** + * 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; + } + + /** + * Retrieves the Search API query object associated with the current view. + * + * @return SearchApiQueryInterface|null + * The Search API query object associated with the current view; or NULL if + * there is none. + */ + protected function getSearchApiQuery() { + if (!isset($this->search_api_query)) { + $this->search_api_query = FALSE; + if (isset($this->view->query) && $this->view->query instanceof SearchApiViewsQuery) { + $this->search_api_query = $this->view->query->getSearchApiQuery(); + } + } + + return $this->search_api_query ? $this->search_api_query : NULL; + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_content_cache.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_content_cache.inc new file mode 100644 index 0000000..555fe89 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_content_cache.inc @@ -0,0 +1,146 @@ +get_results_key(); + $results = NULL; + $query_plugin = $this->view->query; + if ($query_plugin instanceof SearchApiViewsQuery) { + $results = $query_plugin->getSearchApiResults(); + } + $data = array( + 'result' => $this->view->result, + 'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0, + 'current_page' => $this->view->get_current_page(), + 'search_api results' => $results, + ); + cache_set($cid, $data, $this->table, $this->cache_set_expire($type)); + } + + /** + * Overrides views_plugin_cache::cache_get(). + * + * Additionally stores successfully retrieved results with + * search_api_current_search(). + */ + public function cache_get($type) { + if ($type != 'results') { + return parent::cache_get($type); + } + + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->current_page. + if ($cache = cache_get($this->get_results_key(), $this->table)) { + $cutoff = $this->cache_expire($type); + if (!$cutoff || $cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->set_current_page($cache->data['current_page']); + $this->view->execute_time = 0; + + // Trick Search API into believing a search happened, to make facetting + // et al. work. + $query = $this->getSearchApiQuery(); + search_api_current_search($query->getOption('search id'), $query, $cache->data['search_api results']); + + return TRUE; + } + } + return FALSE; + } + + /** + * Overrides views_plugin_cache::get_cache_key(). + * + * Use the Search API query as the main source for the key. Note that in + * Views < 3.8, this method does not exist. + */ + public function get_cache_key($key_data = array()) { + global $user; + + if (!isset($this->_results_key)) { + $query = $this->getSearchApiQuery(); + $query->preExecute(); + $key_data += array( + 'query' => $query, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'language' => $GLOBALS['language']->language, + 'base_url' => $GLOBALS['base_url'], + '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 + // Search API query object we use. + if (isset($_GET['exposed_info'])) { + $key_data['exposed_info'] = $_GET['exposed_info']; + } + } + $key = drupal_hash_base64(serialize($key_data)); + return $key; + } + + /** + * 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; + } + + /** + * Retrieves the Search API query object associated with the current view. + * + * @return SearchApiQueryInterface|null + * The Search API query object associated with the current view; or NULL if + * there is none. + */ + protected function getSearchApiQuery() { + if (!isset($this->search_api_query)) { + $this->search_api_query = FALSE; + if (isset($this->view->query) && $this->view->query instanceof SearchApiViewsQuery) { + $this->search_api_query = $this->view->query->getSearchApiQuery(); + } + } + + return $this->search_api_query ? $this->search_api_query : NULL; + } + +} diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc old mode 100755 new mode 100644 index a6ab7e7..ae58b9a --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc @@ -1,5 +1,10 @@ index = search_api_index_load($id); $this->query = $this->index->query(array( - 'parse mode' => 'terms', + 'parse mode' => $this->options['parse_mode'], )); } } @@ -110,29 +122,81 @@ 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); + if (!$this->errors) { + $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. * - * Adds an option to bypass access checks. + * Adds some access options. */ public function option_definition() { return parent::option_definition() + array( 'search_api_bypass_access' => array( 'default' => FALSE, ), + 'entity_access' => array( + 'default' => FALSE, + ), + 'parse_mode' => array( + 'default' => 'terms', + ), ); } @@ -150,12 +214,45 @@ class SearchApiViewsQuery extends views_plugin_query { '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'), '#default_value' => $this->options['search_api_bypass_access'], ); + + if ($this->index && $this->index->getEntityType()) { + $form['entity_access'] = array( + '#type' => 'checkbox', + '#title' => t('Additional access checks on result entities'), + '#description' => t("Execute an access check for all result entities. This prevents users from seeing inappropriate content when the index contains stale data, or doesn't provide access checks. However, result counts, paging and other things won't work correctly if results are eliminated in this way, so only use this as a last ressort (and in addition to other checks, if possible)."), + '#default_value' => $this->options['entity_access'], + ); + } + + $form['parse_mode'] = array( + '#type' => 'select', + '#title' => t('Parse mode'), + '#description' => t('Choose how the search keys will be parsed.'), + '#options' => array(), + '#default_value' => $this->options['parse_mode'], + ); + foreach ($this->query->parseModes() as $key => $mode) { + $form['parse_mode']['#options'][$key] = $mode['name']; + if (!empty($mode['description'])) { + $states['visible'][':input[name="query[options][parse_mode]"]']['value'] = $key; + $form["parse_mode_{$key}_description"] = array( + '#type' => 'item', + '#title' => $mode['name'], + '#description' => $mode['description'], + '#states' => $states, + ); + } + } } /** * 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. @@ -173,6 +270,7 @@ class SearchApiViewsQuery extends views_plugin_query { // Add a nested filter for each filter group, with its set conjunction. foreach ($this->where as $group_id => $group) { if (!empty($group['conditions']) || !empty($group['filters'])) { + $group += array('type' => 'AND'); // For filters without a group, we want to always add them directly to // the query. $filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']); @@ -199,10 +297,32 @@ class SearchApiViewsQuery extends views_plugin_query { $view->init_pager(); $this->pager->query(); + // 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); + } + // Add the "search_api_bypass_access" option to the query, if desired. if (!empty($this->options['search_api_bypass_access'])) { $this->query->setOption('search_api_bypass_access', TRUE); } + + // 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); + } + + // Save query information for Views UI. + $view->build_info['query'] = (string) $this->query; + } + + /** + * {@inheritdoc} + */ + public function alter(&$view) { + parent::alter($view); + drupal_alter('search_api_views_query', $view, $this); } /** @@ -213,7 +333,7 @@ class SearchApiViewsQuery extends views_plugin_query { * $view->pager['current_page']. */ public function execute(&$view) { - if ($this->errors) { + if ($this->errors || $this->abort) { if (error_displayable()) { foreach ($this->errors as $msg) { drupal_set_message(check_plain($msg), 'error'); @@ -225,24 +345,44 @@ 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 || (!$this->pager->use_count_query() && empty($view->get_total_rows)); + $this->query->setOption('skip result count', $skip_result_count); + } + try { - $start = microtime(TRUE); - // Add range and search ID (if it wasn't already set). - $this->query->range($this->offset, $this->limit); - if ($this->query->getOption('search id') == get_class($this->query)) { - $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display); + // Trigger pager pre_execute(). + 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 + // 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. $results = $this->query->execute(); $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 (!$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']; + } + $this->pager->update_page_info(); } - $this->pager->update_page_info(); $view->result = array(); if (!empty($results['results'])) { $this->addResults($results['results'], $view); @@ -250,14 +390,35 @@ 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(). + if ($this->pager) { + $this->pager->post_execute($view->result); + } } catch (Exception $e) { $this->errors[] = $e->getMessage(); // Recursion to get the same error behaviour as above. - return $this->execute($view); + $this->execute($view); } } + /** + * Aborts this search 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($msg = NULL) { + if ($msg) { + $this->errors[] = $msg; + } + $this->abort = TRUE; + } + /** * Helper function for adding results to a view in the format expected by the * view. @@ -270,6 +431,12 @@ 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_type = $this->index->getEntityType())) { + $entity = $this->index->loadItems(array($id)); + if (!$entity || !entity_access('view', $entity_type, reset($entity))) { + continue; + } + } $row = array(); // Include the loaded item for this result row, if present, or the item @@ -286,11 +453,11 @@ 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. - $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'])) { @@ -308,14 +475,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. @@ -353,12 +520,21 @@ class SearchApiViewsQuery extends views_plugin_query { public function get_result_entities($results, $relationship = NULL, $field = NULL) { list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field); $return = array(); - foreach ($wrappers as $id => $wrapper) { + foreach ($wrappers as $i => $wrapper) { try { - $return[$id] = $wrapper->value(); + // Get the entity ID beforehand for possible watchdog messages. + $id = $wrapper->value(array('identifier' => TRUE)); + + // Only add results that exist. + if ($entity = $wrapper->value()) { + $return[$i] = $entity; + } + else { + watchdog('search_api_views', 'The search index returned a reference to an entity with ID @id, which does not exist in the database. Your index may be out of sync and should be rebuilt.', array('@id' => $id), WATCHDOG_ERROR); + } } catch (EntityMetadataWrapperException $e) { - // Ignore. + watchdog_exception('search_api_views', $e, "%type while trying to load search result entity with ID @id: !message in %function (line %line of %file).", array('@id' => $id), WATCHDOG_ERROR); } } return array($type, $return); @@ -371,31 +547,31 @@ class SearchApiViewsQuery extends views_plugin_query { * query backend. */ public function get_result_wrappers($results, $relationship = NULL, $field = NULL) { - $is_entity = (boolean) entity_get_info($this->index->item_type); + $type = $this->index->getEntityType() ? $this->index->getEntityType() : $this->index->item_type; $wrappers = array(); - $load_entities = array(); + $load_items = array(); foreach ($results as $row_index => $row) { - if ($is_entity && 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($this->index->item_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 = $this->index->item_type; $selector_suffix = ''; if ($field && ($pos = strrpos($field, ':'))) { $selector_suffix = substr($field, 0, $pos); @@ -441,9 +617,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); } } @@ -541,16 +717,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() { diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.api.php b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.api.php new file mode 100644 index 0000000..95a92ca --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info index e0afa1c..2f0bfd7 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info @@ -1,30 +1,36 @@ - -name = Search views +name = Search Views description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments. -dependencies[] = search_api -dependencies[] = views +dependencies[] = search_api:search_api +dependencies[] = views:views core = 7.x package = Search -; Views handlers +; Views handlers/plugins files[] = includes/display_facet_block.inc files[] = includes/handler_argument.inc files[] = includes/handler_argument_fulltext.inc files[] = includes/handler_argument_more_like_this.inc -files[] = includes/handler_argument_text.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 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 +files[] = includes/handler_filter_user.inc files[] = includes/handler_sort.inc +files[] = includes/plugin_cache.inc +files[] = includes/plugin_content_cache.inc files[] = includes/query.inc -; Information added by drupal.org packaging script on 2013-01-09 -version = "7.x-1.4" +; Information added by Drupal.org packaging script on 2019-03-11 +version = "7.x-1.26" core = "7.x" project = "search_api" -datestamp = "1357726719" - +datestamp = "1552334832" diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install index 804d307..03e610b 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install @@ -1,4 +1,5 @@ $view) { + foreach (views_get_all_views() as $view) { if (empty($view->base_table) || empty($table_fields[$view->base_table])) { continue; } @@ -32,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); @@ -66,8 +67,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/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module index 7a65d73..a927f08 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module @@ -1,5 +1,10 @@ enabled && $index->original->enabled) { + // Check whether index was disabled. + $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. + $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 the index was enabled or its fields changed, invalidate the Views cache. + if ($is_enabled != $was_enabled || $old_fields != $new_fields) { + views_invalidate_cache(); } } @@ -30,7 +50,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); + } } /** diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc index 3bb0fc0..afec2d8 100644 --- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc +++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc @@ -1,12 +1,16 @@ machine_name; @@ -20,14 +24,16 @@ 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->item_type])) { - $table['table'] += array( - 'entity type' => $index->item_type, - 'skip entity load' => TRUE, - ); - } + $table['table']['entity type'] = $index->getEntityType(); + $table['table']['skip entity load'] = TRUE; - $wrapper = $index->entityWrapper(NULL, TRUE); + try { + $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); + continue; + } // Add field handlers and relationships provided by the Entity API. foreach ($wrapper as $key => $property) { @@ -37,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; @@ -63,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']; @@ -85,6 +99,7 @@ function search_api_views_views_data() { $table['search_api_relevance']['title'] = t('Relevance'); $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query.'); $table['search_api_relevance']['field']['type'] = 'decimal'; + $table['search_api_relevance']['field']['float'] = TRUE; $table['search_api_relevance']['field']['handler'] = 'entity_views_handler_field_numeric'; $table['search_api_relevance']['field']['click sortable'] = TRUE; $table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort'; @@ -105,6 +120,31 @@ function search_api_views_views_data() { $table['search_api_views_more_like_this']['title'] = t('More like this'); $table['search_api_views_more_like_this']['help'] = t('Find similar content.'); $table['search_api_views_more_like_this']['argument']['handler'] = 'SearchApiViewsHandlerArgumentMoreLikeThis'; + + // If there are taxonomy term references indexed in the index, include the + // "Indexed taxonomy term fields" contextual filter. We also save for all + // fields whether they contain only terms of a certain vocabulary, keying + // that information by vocabulary for later ease of use. + $vocabulary_fields = array(); + foreach ($index->getFields() as $key => $field) { + 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 ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) { + $vocabulary_fields[$vocabulary][] = $key; + } + else { + $vocabulary_fields[''][] = $key; + } + } + } + if ($vocabulary_fields) { + $table['search_api_views_taxonomy_term']['group'] = t('Search'); + $table['search_api_views_taxonomy_term']['title'] = t('Indexed taxonomy term fields'); + $table['search_api_views_taxonomy_term']['help'] = t('Search in all indexed taxonomy term fields.'); + $table['search_api_views_taxonomy_term']['argument']['handler'] = 'SearchApiViewsHandlerArgumentTaxonomyTerm'; + $table['search_api_views_taxonomy_term']['argument']['vocabulary_fields'] = $vocabulary_fields; + } } return $data; } @@ -114,8 +154,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']; @@ -130,7 +180,7 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper if ($inner_type == 'text') { $table[$id] += array( 'argument' => array( - 'handler' => 'SearchApiViewsHandlerArgumentText', + 'handler' => 'SearchApiViewsHandlerArgumentString', ), 'filter' => array( 'handler' => 'SearchApiViewsHandlerFilterText', @@ -139,9 +189,10 @@ 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') { $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean'; @@ -149,11 +200,42 @@ 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'; + $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., + // 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 ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) { + $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'; } - $table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument'; + 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'; + } // We can only sort according to single-valued fields. if ($type == $inner_type) { @@ -168,12 +250,27 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper * Implements hook_views_plugins(). */ function search_api_views_views_plugins() { + // Collect all base tables provided by this module. + $bases = array(); + foreach (search_api_index_load_multiple(FALSE) as $index) { + $bases[] = 'search_api_index_' . $index->machine_name; + } + $ret = array( 'query' => array( 'search_api_views_query' => array( 'title' => t('Search API Query'), 'help' => t('Query will be generated and run using the Search API.'), - 'handler' => 'SearchApiViewsQuery' + 'handler' => 'SearchApiViewsQuery', + ), + ), + 'cache' => array( + 'search_api_views_cache' => array( + 'title' => t('Search-specific'), + 'help' => t("Cache Search API views. (Other methods probably won't work with search views.)"), + 'base' => $bases, + 'handler' => 'SearchApiViewsCache', + 'uses options' => TRUE, ), ), ); @@ -192,5 +289,43 @@ function search_api_views_views_plugins() { ); } + if (module_exists('views_content_cache')) { + $ret['cache']['search_api_views_content_cache'] = array( + 'title' => t('Search-specific content-based'), + 'help' => t("Cache Search API views based on content updates. (Requires Views Content Cache)"), + 'base' => $bases, + 'handler' => 'SearchApiViewsContentCache', + 'uses options' => TRUE, + ); + } + 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; +} diff --git a/sites/all/modules/contrib/search/search_api/includes/callback.inc b/sites/all/modules/contrib/search/search_api/includes/callback.inc index ae4dbf0..617aee3 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback.inc @@ -1,5 +1,13 @@ index = $index; @@ -137,64 +147,28 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI } /** - * 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 - * 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 - * and at least throw an exception with a descriptive error message if this is - * violated on runtime. + * Implements SearchApiAlterCallbackInterface::supportsIndex(). * * The default implementation always returns TRUE. - * - * @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) { return TRUE; } /** - * Display a form for configuring this callback. - * - * @return array - * A form array for configuring this callback, or FALSE if no configuration - * is possible. + * Implements SearchApiAlterCallbackInterface::configurationForm(). */ public function configurationForm() { return array(); } /** - * Validation callback for the form returned by configurationForm(). - * - * @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. + * Implements SearchApiAlterCallbackInterface::configurationFormValidate(). */ public function configurationFormValidate(array $form, array &$values, array &$form_state) { } /** - * 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. + * Implements SearchApiAlterCallbackInterface::configurationFormSubmit(). */ public function configurationFormSubmit(array $form, array &$values, array &$form_state) { $this->options = $values; @@ -202,19 +176,25 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI } /** - * 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). + * Implements SearchApiAlterCallbackInterface::propertyInfo(). */ public function propertyInfo() { 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/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc b/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc index 1524686..55ed611 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc @@ -1,17 +1,48 @@ index->getFields(FALSE); $field_options = array(); + $field_properties = 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']; @@ -60,17 +91,31 @@ 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]['fields'] = array( + $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'), '#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( @@ -106,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']); } } @@ -157,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; @@ -173,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': @@ -184,7 +231,22 @@ 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 'last': + return isset($b) ? $b : $a; + case 'list': + if (!isset($a)) { + $a = array(); + } + $a[] = $b; + return $a; } + return NULL; } /** @@ -237,10 +299,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) { @@ -252,6 +317,9 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'max' => t('Maximum'), 'min' => t('Minimum'), 'first' => t('First'), + 'first_char' => t('First letter'), + 'last' => t('Last'), + 'list' => t('List'), ); case 'type': return array( @@ -260,7 +328,10 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback { 'count' => 'integer', 'max' => 'integer', 'min' => 'integer', - 'first' => 'string', + 'first' => 'token', + 'first_char' => 'token', + 'last' => 'token', + 'list' => 'list', ); case 'description': return array( @@ -270,8 +341,12 @@ 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.'), + '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.'), ); } + return array(); } /** @@ -280,6 +355,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( diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc b/sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc index 4c0a441..b9aada5 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc @@ -1,7 +1,12 @@ 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. @@ -74,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; @@ -82,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)) { @@ -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); @@ -131,22 +108,13 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { $this->extractHierarchy($child, $prop, $values[$key]); } foreach ($values as $key => $value) { - $item->$key = $value; + $item->$key = array_values($value); } } } /** - * 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 @@ -231,13 +199,19 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback { } return; } - $v = $wrapper->value(array('identifier' => TRUE)); - if ($v && !isset($values[$v])) { - $values[$v] = $v; - if (isset($wrapper->$property) && $wrapper->$property->value()) { - $this->extractHierarchy($wrapper->$property, $property, $values); + try { + $v = $wrapper->value(array('identifier' => TRUE)); + if ($v && !isset($values[$v])) { + $values[$v] = $v; + if (isset($wrapper->$property) && $wrapper->value() && $wrapper->$property->value()) { + $this->extractHierarchy($wrapper->$property, $property, $values); + } } } + catch (EntityMetadataWrapperException $e) { + // Some properties like entity_metadata_book_get_properties() throw + // exceptions, so we catch them here and ignore the property. + } } } diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc b/sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc index 097fd41..cc76b33 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc @@ -1,12 +1,17 @@ &$item) { + foreach ($items as &$item) { $url = $this->index->datasource()->getItemUrl($item); if (!$url) { $item->search_api_url = NULL; diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc b/sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc index bd0f8d8..bb2ae07 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc @@ -1,5 +1,10 @@ item_type); + return (bool) $index->getEntityType(); } public function configurationForm() { - $info = entity_get_info($this->index->item_type); $view_modes = array(); - foreach ($info['view modes'] as $key => $mode) { - $view_modes[$key] = $mode['label']; + if ($entity_type = $this->index->getEntityType()) { + $info = entity_get_info($entity_type); + foreach ($info['view modes'] as $key => $mode) { + $view_modes[$key] = $mode['label']; + } } $this->options += array('mode' => reset($view_modes)); if (count($view_modes) > 1) { @@ -60,14 +67,14 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback { $original_user = $GLOBALS['user']; $GLOBALS['user'] = drupal_anonymous_user(); - $type = $this->index->item_type; + $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. 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; diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc b/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc index 4c758c3..cde8fe2 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc @@ -1,55 +1,113 @@ item_type)) && self::hasBundles($info); + 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->item_type); - 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; + } + + $multi_entity = $this->isMultiEntityIndex(); + if ($multi_entity) { + $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) { + // 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]); } } } + /** + * {@inheritdoc} + */ public function configurationForm() { - $info = entity_get_info($this->index->item_type); - 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))) . '

'; + } } - $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, + 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 = array_flip($this->index->options['datasource']['bundles']); + $options = array_intersect_key($options, $included_bundles); + } + $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( @@ -62,8 +120,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, FALSE otherwise. */ protected static function hasBundles(array $entity_info) { return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']); diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_comment_access.inc b/sites/all/modules/contrib/search/search_api/includes/callback_comment_access.inc new file mode 100644 index 0000000..e627353 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/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/sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc b/sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc index 0ac481f..d406d35 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc @@ -1,5 +1,10 @@ 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') . ']'; } @@ -98,19 +89,7 @@ class SearchApiAlterLanguageControl 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) { $values['languages'] = array_filter($values['languages']); @@ -118,15 +97,7 @@ class SearchApiAlterLanguageControl 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) { foreach ($items as $i => &$item) { diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc b/sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc index ce671d7..8bfab49 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc @@ -10,31 +10,19 @@ 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. - return $index->item_type === 'node'; + return $index->getEntityType() === 'node'; } /** - * 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/sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc b/sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc index bfe35e2..ee57c38 100644 --- a/sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc +++ b/sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc @@ -22,7 +22,7 @@ class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback { * TRUE if the callback can run on the given index; FALSE otherwise. */ public function supportsIndex(SearchApiIndex $index) { - return $index->item_type === 'node'; + return $index->getEntityType() === 'node'; } /** diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc b/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc new file mode 100644 index 0000000..68b8d72 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc @@ -0,0 +1,78 @@ +isMultiEntityIndex($index)) { + return in_array('user', $index->options['datasource']['types']); + } + return $index->getEntityType() == 'user'; + } + + /** + * Implements SearchApiAlterCallbackInterface::alterItems(). + */ + public function alterItems(array &$items) { + $selected_roles = $this->options['roles']; + $default = (bool) $this->options['default']; + $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]); + } + } + } + + /** + * Overrides SearchApiAbstractAlterCallback::configurationForm(). + * + * Add option for the roles to include/exclude. + */ + public function configurationForm() { + $options = array_map('check_plain', user_roles()); + $form = array( + 'default' => array( + '#type' => 'radios', + '#title' => t('Which users 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 roles'), + 0 => t('Only those from the selected roles'), + ), + ), + 'roles' => array( + '#type' => 'select', + '#title' => t('Roles'), + '#default_value' => isset($this->options['roles']) ? $this->options['roles'] : array(), + '#options' => $options, + '#size' => min(4, count($options)), + '#multiple' => TRUE, + ), + ); + return $form; + } + +} diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_user_content.inc b/sites/all/modules/contrib/search/search_api/includes/callback_user_content.inc new file mode 100644 index 0000000..168f3ae --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/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/sites/all/modules/contrib/search/search_api/includes/callback_user_status.inc b/sites/all/modules/contrib/search/search_api/includes/callback_user_status.inc new file mode 100644 index 0000000..541f1b2 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/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/sites/all/modules/contrib/search/search_api/includes/datasource.inc b/sites/all/modules/contrib/search/search_api/includes/datasource.inc old mode 100755 new mode 100644 index 21a6157..9661092 --- a/sites/all/modules/contrib/search/search_api/includes/datasource.inc +++ b/sites/all/modules/contrib/search/search_api/includes/datasource.inc @@ -12,45 +12,55 @@ * They are used for loading items, extracting item data, keeping track of the * item status, etc. * - * All methods of the data source may throw exceptions of type - * SearchApiDataSourceException if any exception or error state is encountered. + * Modules providing implementations of this interface that use a different way + * (either different table or different method altogether) of keeping track of + * indexed/dirty items than SearchApiAbstractDataSourceController should be + * 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. */ interface SearchApiDataSourceControllerInterface { /** - * Constructor for a data source controller. + * Constructs an SearchApiDataSourceControllerInterface object. * - * @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. + * The IDs of the items to load. * * @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 @@ -60,151 +70,182 @@ 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 * 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 - * 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. * - * @param array $indexes + * @param SearchApiIndex[] $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. + * 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. - * @param array $indexes + * @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 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 + * @param SearchApiIndex[] $indexes * The indexes for which the change should be tracked. - * @param $dequeue - * If set to TRUE, also change the status of queued items. + * @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. + * + * @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 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 any of the indexes 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, + * 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); /** - * 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. - * @param SearchApiIndex $indexes + * @param SearchApiIndex $index * 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. - * @param array $indexes + * @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 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 @@ -212,16 +253,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. @@ -233,14 +277,89 @@ 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); + /** + * 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(); + + /** + * 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); + } /** - * 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 @@ -264,6 +383,15 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou */ protected $type; + /** + * The entity type for this controller instance. + * + * @var string|null + * + * @see getEntityType() + */ + protected $entityType = NULL; + /** * The info array for the item type, as specified via * hook_search_api_item_type_info(). @@ -306,57 +434,86 @@ 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; $this->info = search_api_get_item_type_info($type); + + if (!empty($this->info['entity_type'])) { + $this->entityType = $this->info['entity_type']; + } } /** - * 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 getEntityType() { + return $this->entityType; + } + + /** + * {@inheritdoc} */ public function getMetadataWrapper($item = NULL, array $info = array()) { $info += $this->getPropertyInfo(); - return entity_metadata_wrapper($this->type, $item, $info); + return entity_metadata_wrapper($this->entityType ? $this->entityType : $this->type, $item, $info); } /** - * Helper method that can be used by subclasses to specify the property - * information to use when creating a metadata wrapper. + * 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 + * metadata wrapper. + * + * 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 "properties" key is called + * "property info" instead. So, an example return value would look like this: + * + * @code + * return array( + * 'property info' => array( + * 'foo' => array( + * 'label' => t('Foo'), + * 'type' => 'text', + * ), + * 'bar' => array( + * 'label' => t('Bar'), + * 'type' => 'list', + * ), + * ), + * ); + * @endcode + * + * SearchApiExternalDataSourceController::getPropertyInfo() contains a working + * example of this method. + * + * If the item type is an entity type, no additional property information is + * required, the method will thus just return an empty array. You can still + * use this to append additional properties to the entities, or the like, + * though. * * @return array - * Property information as specified by hook_entity_property_info(). + * Property information as specified by entity_metadata_wrapper(). * + * @throws SearchApiDataSourceException + * If any error state was encountered. + * + * @see getMetadataWrapper() * @see hook_entity_property_info() */ protected function getPropertyInfo() { + // If this is an entity type, no additional property info is needed. + if ($this->entityType) { + return array(); + } throw new SearchApiDataSourceException(t('No known property information for type @type.', array('@type' => $this->type))); } /** - * 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(); @@ -370,13 +527,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(); @@ -384,33 +535,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) { @@ -424,27 +556,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) { @@ -454,28 +582,24 @@ 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(); } } /** - * 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) { + if (!$this->table || $item_ids === array()) { 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) { @@ -483,7 +607,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, @@ -496,60 +619,53 @@ 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) { - return; + if (!$this->table || $item_ids === array()) { + return NULL; } - $index_ids = array(); + + $indexes_by_id = 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->id) + ->condition($this->changedColumn, 0, $dequeue ? '<=' : '='); + if ($item_ids !== FALSE) { + $update->condition($this->itemIdColumn, $item_ids, 'IN'); + } + $update->execute(); + $indexes_by_id[$index->id] = $index; } - $update = db_update($this->table) - ->fields(array( - $this->changedColumn => REQUEST_TIME, - )) - ->condition($this->indexIdColumn, $index_ids, 'IN') - ->condition($this->changedColumn, 0, $dequeue ? '<=' : '='); + + // 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) { - $update->condition($this->itemIdColumn, $item_ids, 'IN'); + $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)); } - $update->execute(); + + return NULL; } /** - * 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) { + $this->checkIndex($index); + if (!$this->table || $item_ids === array()) { return; } $update = db_update($this->table) @@ -564,18 +680,10 @@ 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) { + if (!$this->table || $item_ids === array()) { return; } $this->checkIndex($index); @@ -589,45 +697,30 @@ 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) { - return; + if (!$this->table || $item_ids === array()) { + 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; } /** - * 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) { @@ -635,7 +728,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'); @@ -646,16 +739,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) { @@ -677,20 +761,51 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou } /** - * Helper method for ensuring that an index uses the same item type as this controller. + * {@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. + * + * 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) { $index_type = search_api_get_item_type_info($index->item_type); $index_type = empty($index_type['name']) ? $index->item_type : $index_type['name']; - $msg = t('Invalid index @index of type @index_type passed to data source controller for type @this_type.', - array('@index' => $index->name, '@index_type' => $index_type, '@this_type' => $this->info['name'])); + $msg = t( + 'Invalid index @index of type @index_type passed to data source controller for type @this_type.', + array('@index' => $index->name, '@index_type' => $index_type, '@this_type' => $this->info['name']) + ); throw new SearchApiDataSourceException($msg); } } diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc index 641a939..bbca0aa 100644 --- a/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc +++ b/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc @@ -6,53 +6,82 @@ */ /** - * 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. + * Entity type info for this 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. + * @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->type); - $properties = entity_get_property_info($this->type); - if (empty($info['entity keys']['id'])) { - throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label']))); + $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']))); } - $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, ); } /** - * 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->type, $ids); + $items = entity_load($this->entityType, $ids); // If some items couldn't be loaded, remove them from tracking. if (count($items) != count($ids)) { $ids = array_flip($ids); @@ -65,65 +94,33 @@ 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->type, $item, $info); + 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->type, $item); + $id = entity_id($this->entityType, $item); return $id ? $id : NULL; } /** - * 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->type, $item); + $label = entity_label($this->entityType, $item); return $label ? $label : NULL; } /** - * 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->type == 'file') { + if ($this->entityType == 'file') { return array( 'path' => file_create_url($item->uri), 'options' => array( @@ -132,23 +129,12 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon ), ); } - $url = entity_uri($this->type, $item); + $url = entity_uri($this->entityType, $item); return $url ? $url : 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) { @@ -158,24 +144,60 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon // all items again without any key conflicts. $this->stopTracking($indexes); - $entity_info = entity_get_info($this->type); - - 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)) { + $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(); + } + 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, + // 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 { + continue; + } + } + else { + $this->startTrackingFallback(array($index->machine_name => $index)); + continue; + } + } + if ($bundles) { + $query->condition($bundle_column, $bundles); + } + } // INSERT ... SELECT ... db_insert($this->table) @@ -184,23 +206,165 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon } } else { - // In the absence of a 'base table', use the slow entity_load(). - parent::startTracking($indexes); + $this->startTrackingFallback($indexes); } } /** - * Helper method that can be used by subclasses instead of implementing startTracking(). + * Initializes tracking of the index status of items for the given indexes. * - * Returns the IDs of all items that are known for this controller's type. + * Fallback for when the items cannot directly be loaded into + * {search_api_item} via "INSERT INTO … SELECT …". * - * Will be used when the entity type doesn't specify a "base table". + * @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)); + } + } + } + + /** + * {@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 enabled indexes.'), + '#options' => array_map('check_plain', $options), + '#attributes' => array('class' => array('search-api-checkboxes-list')), + '#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']); + } + 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} + */ + 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 containing all item IDs for this type. + * An array (which might be empty) mapping this entity type's bundle keys to + * their labels. */ - protected function getAllItemIds() { - return array_keys(entity_load($this->type)); + 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/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc index 3dbb17c..c3a9f1f 100644 --- a/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc +++ b/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc @@ -19,9 +19,7 @@ * will only have to specify some property information in getPropertyInfo(). If * you have a custom service class which already returns the extracted fields * with the search results, you will only have to provide a label and a type for - * each field. To make this use case easier, there is also a - * getFieldInformation() method which you can implement instead of directly - * implementing getPropertyInfo(). + * each field. */ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceController { @@ -51,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. @@ -61,16 +59,9 @@ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceC } /** - * Helper method that can be used by subclasses to specify the property - * information to use when creating a metadata wrapper. + * Overrides SearchApiAbstractDataSourceController::getPropertyInfo(). * - * For most use cases, you will have to override this method to provide the - * real property information for your item type. - * - * @return array - * Property information as specified by hook_entity_property_info(). - * - * @see hook_entity_property_info() + * Only returns a single string ID field. */ protected function getPropertyInfo() { $info['property info']['id'] = array( diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc new file mode 100644 index 0000000..ea16496 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc @@ -0,0 +1,360 @@ + '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; + // 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; + } + 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' => 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, + ); + 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/sites/all/modules/contrib/search/search_api/includes/exception.inc b/sites/all/modules/contrib/search/search_api/includes/exception.inc index 4e7a0c8..2e4f790 100644 --- a/sites/all/modules/contrib/search/search_api/includes/exception.inc +++ b/sites/all/modules/contrib/search/search_api/includes/exception.inc @@ -1,5 +1,10 @@ fields[$only_indexed][$get_additional]. + * + * @var array + */ + protected $fields = array(); + /** * An array containing two arrays. * @@ -101,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. @@ -136,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 */ @@ -158,29 +175,25 @@ 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); } /** * Execute necessary tasks for a newly created index. */ public function postCreate() { - if ($this->enabled) { - $this->queueItems(); - } - $server = $this->server(); - if ($server) { - // Tell the server about the new index. - if ($server->enabled) { + try { + if ($server = $this->server()) { + // Tell the server about the new index. $server->addIndex($this); + if ($this->enabled) { + $this->queueItems(); + } } - 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); - } + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); } } @@ -188,15 +201,13 @@ class SearchApiIndex extends Entity { * Execute necessary tasks when the index is removed from the database. */ public function postDelete() { - if ($server = $this->server()) { - if ($server->enabled) { + try { + if ($server = $this->server()) { $server->removeIndex($this); } - else { - $tasks = variable_get('search_api_tasks', array()); - $tasks[$server->machine_name][$this->machine_name] = array('remove'); - variable_set('search_api_tasks', $tasks); - } + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); } // Stop tracking entities for indexing. @@ -208,7 +219,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); + } } } @@ -216,15 +232,20 @@ class SearchApiIndex extends Entity { * Remove all records of entities to index. */ public function dequeueItems() { - $this->datasource()->stopTracking(array($this)); - _search_api_empty_cron_queue($this); + try { + $this->datasource()->stopTracking(array($this)); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } /** - * 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. @@ -233,15 +254,25 @@ class SearchApiIndex extends Entity { if (empty($this->description)) { $this->description = NULL; } - if (empty($this->server)) { + $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); + } + } + if (!$server) { $this->server = NULL; $this->enabled = FALSE; } - // This will also throw an exception if the server doesn't exist – which is good. - elseif (!$this->server(TRUE)->enabled) { - $this->enabled = FALSE; + if (!empty($this->options['fields'])) { + ksort($this->options['fields']); } + $this->resetCaches(); + return parent::save(); } @@ -254,7 +285,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. */ @@ -283,7 +314,7 @@ class SearchApiIndex extends Entity { /** * Schedules this search index for re-indexing. * - * @return + * @return bool * TRUE on success, FALSE on failure. */ public function reindex() { @@ -298,7 +329,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() { @@ -306,19 +337,11 @@ class SearchApiIndex extends Entity { return TRUE; } - $server = $this->server(); - if ($server->enabled) { - $server->deleteItems('all', $this); + try { + $this->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); - } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); } _search_api_index_reindex($this); @@ -356,6 +379,22 @@ class SearchApiIndex extends Entity { return $this->datasource; } + /** + * Get the entity type of items in this index. + * + * @return string|null + * An entity type string if the items in this index are entities; NULL + * otherwise. + */ + public function getEntityType() { + try { + return $this->datasource()->getEntityType(); + } + catch (SearchApiException $e) { + return NULL; + } + } + /** * Get the server this index lies on. * @@ -388,7 +427,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. @@ -400,17 +439,21 @@ class SearchApiIndex extends Entity { return $this->server()->query($this, $options); } - /** - * 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) { @@ -528,8 +571,8 @@ class SearchApiIndex extends Entity { 'options list' => 'entity_metadata_language_list', ), ); - // We use the reverse order here so the hierarchy for overwriting property infos is the same - // as for actually overwriting the properties. + // We use the reverse order here so the hierarchy for overwriting property + // infos is the same as for actually overwriting the properties. foreach (array_reverse($this->getAlterCallbacks()) as $callback) { $props = $callback->propertyInfo(); if ($props) { @@ -543,16 +586,14 @@ class SearchApiIndex extends Entity { return $property_info; } - /** - * Fills the $processors array for use by the pre-/postprocessing functions. + /** + * Loads all enabled data alterations for this index in proper order. * - * @return SearchApiIndex - * The called object. * @return array * All enabled callbacks for this index, as SearchApiAlterCallbackInterface * objects. */ - protected function getAlterCallbacks() { + public function getAlterCallbacks() { if (isset($this->callbacks)) { return $this->callbacks; } @@ -585,11 +626,13 @@ class SearchApiIndex extends Entity { } /** + * Loads all enabled processors for this index in proper order. + * * @return array * All enabled processors for this index, as SearchApiProcessorInterface * objects. */ - protected function getProcessors() { + public function getProcessors() { if (isset($this->processors)) { return $this->processors; } @@ -721,140 +764,162 @@ class SearchApiIndex extends Entity { * "additional fields" key. */ public function getFields($only_indexed = TRUE, $get_additional = FALSE) { - $fields = empty($this->options['fields']) ? array() : $this->options['fields']; - $wrapper = $this->entityWrapper(); - $additional = array(); - $entity_types = entity_get_info(); + global $language; - // First we need all already added prefixes. - $added = ($only_indexed || empty($this->options['additional fields'])) ? array() : $this->options['additional fields']; - foreach (array_keys($fields) as $key) { - $len = strlen($key) + 1; - $pos = $len; - // The third parameter ($offset) to strrpos has rather weird behaviour, - // necessitating this rather awkward code. It will iterate over all - // prefixes of each field, beginning with the longest, adding all of them - // to $added until one is encountered that was already added (which means - // all shorter ones will have already been added, too). - while ($pos = strrpos($key, ':', $pos - $len)) { - $prefix = substr($key, 0, $pos); - if (isset($added[$prefix])) { - break; - } - $added[$prefix] = $prefix; + $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-{$language->language}"; + $cache = cache_get($cid); + if ($cache) { + $this->fields[$only_indexed][$get_additional] = $cache->data; } } - // 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. + // Otherwise, we have to compute the result. + if (empty($this->fields[$only_indexed][$get_additional])) { + $fields = empty($this->options['fields']) ? array() : $this->options['fields']; + $wrapper = $this->entityWrapper(); + $additional = array(); + $entity_types = entity_get_info(); - // 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 - $nesting_levels = array('' => 0); - - $types = search_api_default_field_types(); - $flat = array(); - while ($wrappers) { - foreach ($wrappers as $prefix => $wrapper) { - $prefix_name = $prefix_names[$prefix]; - // Deal with lists of entities. - $nesting_level = $nesting_levels[$prefix]; - $type_prefix = str_repeat('list<', $nesting_level); - $type_suffix = str_repeat('>', $nesting_level); - if ($nesting_level) { - $info = $wrapper->info(); - // The real nesting level of the wrapper, not the accumulated one. - $level = search_api_list_nesting_level($info['type']); - for ($i = 0; $i < $level; ++$i) { - $wrapper = $wrapper[0]; + // First we need all already added prefixes. + $added = ($only_indexed || empty($this->options['additional fields'])) ? array() : $this->options['additional fields']; + foreach (array_keys($fields) as $key) { + $len = strlen($key) + 1; + $pos = $len; + // The third parameter ($offset) to strrpos has rather weird behaviour, + // necessitating this rather awkward code. It will iterate over all + // prefixes of each field, beginning with the longest, adding all of them + // to $added until one is encountered that was already added (which means + // all shorter ones will have already been added, too). + while ($pos = strrpos($key, ':', $pos - $len)) { + $prefix = substr($key, 0, $pos); + if (isset($added[$prefix])) { + break; } + $added[$prefix] = $prefix; } - // Now look at all properties. - foreach ($wrapper as $property => $value) { - $info = $value->info(); - // 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. - if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) { - // Inner type is changed to "string". - $type = 'string'; - // Set the field type accordingly. - $info['type'] = search_api_nest_type('string', $info['type']); - } - $info['type'] = $type_prefix . $info['type'] . $type_suffix; - $key = $prefix . $property; - if ((isset($types[$type]) || isset($entity_types[$type])) && (!$only_indexed || !empty($fields[$key]))) { - if (!empty($fields[$key])) { - // This field is already known in the index configuration. - $flat[$key] = $fields[$key] + array( - 'name' => $prefix_name . $info['label'], - 'description' => empty($info['description']) ? NULL : $info['description'], - 'boost' => '1.0', - 'indexed' => TRUE, - ); - // Update the type and its nesting level for non-entity properties. - if (!isset($entity_types[$type])) { - $flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']); - if (isset($flat[$key]['real_type'])) { - $real_type = search_api_extract_inner_type($flat[$key]['real_type']); - $flat[$key]['real_type'] = search_api_nest_type($real_type, $info['type']); - } - } - } - else { - $flat[$key] = array( - 'name' => $prefix_name . $info['label'], - 'description' => empty($info['description']) ? NULL : $info['description'], - 'type' => $info['type'], - 'boost' => '1.0', - 'indexed' => FALSE, - ); - } - if (isset($entity_types[$type])) { - $base_type = isset($entity_types[$type]['entity keys']['name']) ? 'string' : 'integer'; - $flat[$key]['type'] = search_api_nest_type($base_type, $info['type']); - $flat[$key]['entity_type'] = $type; - } - } - if (empty($types[$type])) { - if (isset($added[$key])) { - // Visit this entity/struct in a later iteration. - $wrappers[$key . ':'] = $value; - $prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » '; - $nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']); - } - else { - $name = $prefix_name . $info['label']; - // Add machine names to discern fields with identical labels. - if (isset($used_names[$name])) { - if ($used_names[$name] !== FALSE) { - $additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']'; - $used_names[$name] = FALSE; - } - $name .= ' [' . $key . ']'; - } - $additional[$key] = $name; - $used_names[$name] = $key; - } - } - } - unset($wrappers[$prefix]); } + + // 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 + $wrappers = array('' => $wrapper); + // Display names for the prefixes + $prefix_names = array('' => ''); + // The list nesting level for entities with a certain prefix + $nesting_levels = array('' => 0); + + $types = search_api_default_field_types(); + $flat = array(); + while ($wrappers) { + foreach ($wrappers as $prefix => $wrapper) { + $prefix_name = $prefix_names[$prefix]; + // Deal with lists of entities. + $nesting_level = $nesting_levels[$prefix]; + $type_prefix = str_repeat('list<', $nesting_level); + $type_suffix = str_repeat('>', $nesting_level); + if ($nesting_level) { + $info = $wrapper->info(); + // The real nesting level of the wrapper, not the accumulated one. + $level = search_api_list_nesting_level($info['type']); + for ($i = 0; $i < $level; ++$i) { + $wrapper = $wrapper[0]; + } + } + // Now look at all properties. + foreach ($wrapper as $property => $value) { + $info = $value->info(); + // 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. + if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) { + // Inner type is changed to "string". + $type = 'string'; + // Set the field type accordingly. + $info['type'] = search_api_nest_type('string', $info['type']); + } + $info['type'] = $type_prefix . $info['type'] . $type_suffix; + $key = $prefix . $property; + if ((isset($types[$type]) || isset($entity_types[$type])) && (!$only_indexed || !empty($fields[$key]))) { + if (!empty($fields[$key])) { + // This field is already known in the index configuration. + $flat[$key] = $fields[$key] + array( + 'name' => $prefix_name . $info['label'], + 'description' => empty($info['description']) ? NULL : $info['description'], + 'boost' => '1.0', + 'indexed' => TRUE, + ); + // Update the type and its nesting level for non-entity properties. + if (!isset($entity_types[$type])) { + $flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']); + if (isset($flat[$key]['real_type'])) { + $real_type = search_api_extract_inner_type($flat[$key]['real_type']); + $flat[$key]['real_type'] = search_api_nest_type($real_type, $info['type']); + } + } + } + else { + $flat[$key] = array( + 'name' => $prefix_name . $info['label'], + 'description' => empty($info['description']) ? NULL : $info['description'], + 'type' => $info['type'], + 'boost' => '1.0', + 'indexed' => FALSE, + ); + } + if (isset($entity_types[$type])) { + $base_type = isset($entity_types[$type]['entity keys']['name']) ? 'string' : 'integer'; + $flat[$key]['type'] = search_api_nest_type($base_type, $info['type']); + $flat[$key]['entity_type'] = $type; + } + } + if (empty($types[$type])) { + if (isset($added[$key])) { + // Visit this entity/struct in a later iteration. + $wrappers[$key . ':'] = $value; + $prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » '; + $nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']); + } + else { + $name = $prefix_name . $info['label']; + // Add machine names to discern fields with identical labels. + if (isset($used_names[$name])) { + if ($used_names[$name] !== FALSE) { + $additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']'; + $used_names[$name] = FALSE; + } + $name .= ' [' . $key . ']'; + } + $additional[$key] = $name; + $used_names[$name] = $key; + } + } + } + unset($wrappers[$prefix]); + } + } + + if (!$get_additional) { + $this->fields[$only_indexed][$get_additional] = $flat; + } + else { + $options = array(); + $options['fields'] = $flat; + $options['additional fields'] = $additional; + $this->fields[$only_indexed][$get_additional] = $options; + } + cache_set($cid, $this->fields[$only_indexed][$get_additional]); } - if (!$get_additional) { - return $flat; - } - $options = array(); - $options['fields'] = $flat; - $options['additional fields'] = $additional; - return $options; + return $this->fields[$only_indexed][$get_additional]; } /** @@ -871,7 +936,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; @@ -881,6 +951,19 @@ class SearchApiIndex extends Entity { return $this->fulltext_fields[$i]; } + /** + * Get the cache ID prefix used for this index's caches. + * + * @param $type + * The type of cache. Currently only "fields" is used. + * + * @return + * The cache ID (prefix) for this index's caches. + */ + public function getCacheId($type = 'fields') { + return 'search_api:index-' . $this->machine_name . '--' . $type; + } + /** * Helper function for creating an entity metadata wrapper appropriate for * this index. @@ -895,12 +978,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); + } } /** @@ -915,21 +1004,30 @@ 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(); + } } /** - * 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; $this->processors = NULL; $this->added_properties = NULL; + $this->fields = array(); $this->fulltext_fields = array(); } diff --git a/sites/all/modules/contrib/search/search_api/includes/processor.inc b/sites/all/modules/contrib/search/search_api/includes/processor.inc index 4cfaa4c..4baedbb 100644 --- a/sites/all/modules/contrib/search/search_api/includes/processor.inc +++ b/sites/all/modules/contrib/search/search_api/includes/processor.inc @@ -1,5 +1,10 @@ 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; } @@ -187,7 +192,7 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface public function configurationFormValidate(array $form, array &$values, array &$form_state) { $fields = array_filter($values['fields']); if ($fields) { - $fields = array_combine($fields, array_fill(0, count($fields), TRUE)); + $fields = array_fill_keys($fields, TRUE); } $values['fields'] = $fields; } @@ -272,8 +277,14 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface $this->processFieldValue($value); } if (is_array($value)) { - $type = 'tokens'; - $value = $this->normalizeTokens($value); + // Don't tokenize non-fulltext content! + if (in_array($type, array('text', 'tokens'))) { + $type = 'tokens'; + $value = $this->normalizeTokens($value); + } + else { + $value = $this->implodeTokens($value); + } } } @@ -305,6 +316,32 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface return $ret; } + /** + * Internal helper function for imploding tokens into a single string. + * + * @param array $tokens + * The tokens array to implode. + * + * @return string + * The text data from the tokens concatenated into a single string. + */ + protected function implodeTokens(array $tokens) { + $ret = array(); + foreach ($tokens as $token) { + if (empty($token['value']) && !is_numeric($token['value'])) { + // Filter out empty tokens. + continue; + } + if (is_array($token['value'])) { + $ret[] = $this->implodeTokens($token['value']); + } + else { + $ret[] = $token['value']; + } + } + return implode(' ', $ret); + } + /** * Method for preprocessing search keys. */ @@ -329,10 +366,20 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface */ protected function processFilters(array &$filters) { $fields = $this->index->options['fields']; - foreach ($filters as &$f) { + foreach ($filters as $key => &$f) { if (is_array($f)) { if (isset($fields[$f[0]]) && $this->testField($f[0], $fields[$f[0]])) { + // We want to allow processors also to easily remove complete filters. + // However, we can't use empty() or the like, as that would sort out + // filters for 0 or NULL. So we specifically check only for the empty + // string, and we also make sure the filter value was actually changed + // by storing whether it was empty before. + $empty_string = $f[1] === ''; $this->processFilterValue($f[1]); + + if ($f[1] === '' && !$empty_string) { + unset($filters[$key]); + } } } else { @@ -343,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'])) { @@ -359,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/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc b/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc new file mode 100644 index 0000000..2ac502a --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc @@ -0,0 +1,527 @@ +options += array( + 'prefix' => '', + 'suffix' => '', + 'excerpt' => TRUE, + 'excerpt_length' => 256, + 'highlight' => 'always', + 'highlight_partial' => FALSE, + 'exclude_fields' => array(), + ); + + $form['prefix'] = array( + '#type' => 'textfield', + '#title' => t('Highlighting prefix'), + '#description' => t('Text/HTML that will be prepended to all occurrences of search keywords in highlighted text.'), + '#default_value' => $this->options['prefix'], + ); + $form['suffix'] = array( + '#type' => 'textfield', + '#title' => t('Highlighting suffix'), + '#description' => t('Text/HTML that will be appended to all occurrences of search keywords in highlighted text.'), + '#default_value' => $this->options['suffix'], + ); + $form['excerpt'] = array( + '#type' => 'checkbox', + '#title' => t('Create excerpt'), + '#description' => t('When enabled, an excerpt will be created for searches with keywords, containing all occurrences of keywords in a fulltext field.'), + '#default_value' => $this->options['excerpt'], + ); + $form['excerpt_length'] = array( + '#type' => 'textfield', + '#title' => t('Excerpt length'), + '#description' => t('The requested length of the excerpt, in characters.'), + '#default_value' => $this->options['excerpt_length'], + '#element_validate' => array('element_validate_integer_positive'), + '#states' => array( + 'visible' => array( + '#edit-processors-search-api-highlighting-settings-excerpt' => array( + 'checked' => TRUE, + ), + ), + ), + ); + // Exclude certain fulltext fields. + $fields = $this->index->getFields(); + $fulltext_fields = array(); + foreach ($this->index->getFulltextFields() as $field) { + if (isset($fields[$field])) { + $fulltext_fields[$field] = check_plain($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'), + '#description' => t('Select whether returned fields should be highlighted.'), + '#options' => array( + 'always' => t('Always'), + 'server' => t('If the server returns fields'), + 'never' => t('Never'), + ), + '#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; + } + + /** + * {@inheritdoc} + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state) { + $values['exclude_fields'] = array_filter($values['exclude_fields']); + } + + /** + * {@inheritdoc} + */ + public function postprocessSearchResults(array &$response, SearchApiQuery $query) { + if (empty($response['results']) || !($keys = $this->getKeywords($query))) { + 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, $fulltext_fields); + foreach ($fields as $data) { + if (is_array($data)) { + $text = array_merge($text, $data); + } + else { + $text[] = $data; + } + } + + $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'); + foreach ($fields as $field => $data) { + $result['fields'][$field] = array('#sanitize_callback' => FALSE); + if (is_array($data)) { + foreach ($data as $i => $text) { + $result['fields'][$field]['#value'][$i] = $this->highlightField($text, $keys); + } + } + else { + $result['fields'][$field]['#value'] = $this->highlightField($data, $keys); + } + } + } + } + } + + /** + * Retrieves the fulltext data of a result. + * + * @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. + * @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. + * + * @return array + * An array containing fulltext field names mapped to the text data + * contained in them for the given result. + */ + protected function getFulltextFields(array &$results, $i, array $fulltext_fields, $load = TRUE) { + global $language; + $data = array(); + + $result = &$results[$i]; + // Act as if $load is TRUE if we have a loaded item. + $load |= !empty($result['entity']); + $result += array('fields' => array()); + // 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, $returned_fields)) { + $data[$field] = $returned_fields[$field]; + } + elseif ($load) { + $needs_extraction[$field] = $fields[$field]; + } + } + + if (!$needs_extraction) { + return $data; + } + + if (empty($result['entity'])) { + $items = $this->index->loadItems(array_keys($results)); + foreach ($items as $id => $item) { + $results[$id]['entity'] = $item; + } + } + // If we still don't have a loaded item, we should stop trying. + if (empty($result['entity'])) { + return $data; + } + $wrapper = $this->index->entityWrapper($result['entity'], FALSE); + $wrapper->language($language->language); + $extracted = search_api_extract_fields($wrapper, $needs_extraction, array('sanitize' => TRUE)); + + foreach ($extracted as $field => $info) { + if (isset($info['value'])) { + $data[$field] = $info['value']; + } + } + + return $data; + } + + /** + * Extracts the positive keywords used in a search query. + * + * @param SearchApiQuery $query + * The query from which to extract the keywords. + * + * @return array + * An array of all unique positive keywords used in the query. + */ + protected function getKeywords(SearchApiQuery $query) { + $keys = $query->getKeys(); + if (!$keys) { + return array(); + } + if (is_array($keys)) { + return $this->flattenKeysArray($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)); + // Remove quotes from keywords. + foreach ($keywords as $key) { + $keywords[$key] = trim($key, "'\""); + } + return drupal_map_assoc(array_filter($keywords)); + } + + /** + * Extracts the positive keywords from a keys array. + * + * @param array $keys + * A search keys array, as specified by SearchApiQueryInterface::getKeys(). + * + * @return array + * An array of all unique positive keywords contained in the keys. + */ + protected function flattenKeysArray(array $keys) { + if (!empty($keys['#negation'])) { + return array(); + } + + $keywords = array(); + foreach ($keys as $i => $key) { + if (!element_child($i)) { + continue; + } + if (is_array($key)) { + $keywords += $this->flattenKeysArray($key); + } + else { + $keywords[$key] = $key; + } + } + + return $keywords; + } + + /** + * Returns snippets from a piece of text, with certain keywords highlighted. + * + * Largely copied from search_excerpt(). + * + * @param string $text + * The text to extract fragments from. + * @param array $keys + * Search keywords entered by the user. + * + * @return string|null + * A string containing HTML for the excerpt, or NULL if none could be + * created. + */ + protected function createExcerpt($text, array $keys) { + // Prepare text by stripping HTML tags and decoding HTML entities. + $text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text)); + $text = decode_entities($text); + $text = preg_replace('/\s+/', ' ', $text); + $text = trim($text, ' '); + $text_length = strlen($text); + + // Try to reach the requested excerpt length with about two fragments (each + // with a keyword and some context). + $ranges = array(); + $length = 0; + $look_start = array(); + $remaining_keys = $keys; + + // Get the set excerpt length from the configuration. If the length is too + // small, only use one fragment. + $excerpt_length = $this->options['excerpt_length']; + $context_length = round($excerpt_length / 4) - 3; + if ($context_length < 32) { + $context_length = round($excerpt_length / 2) - 1; + } + + while ($length < $excerpt_length && !empty($remaining_keys)) { + $found_keys = array(); + foreach ($remaining_keys as $key) { + if ($length >= $excerpt_length) { + break; + } + + // Remember where we last found $key, in case we are coming through a + // second time. + if (!isset($look_start[$key])) { + $look_start[$key] = 0; + } + + // See if we can find $key after where we found it the last time. Since + // we are requiring a match on a word boundary, make sure $text starts + // and ends with a space. + $matches = array(); + + if (!$this->options['highlight_partial']) { + $found_position = FALSE; + $regex = '/' . static::$boundary . preg_quote($key, '/') . static::$boundary . '/iu'; + if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) { + $found_position = $matches[0][1]; + } + } + else { + $found_position = stripos($text, $key, $look_start[$key]); + } + if ($found_position !== FALSE) { + $look_start[$key] = $found_position + 1; + // Keep track of which keys we found this time, in case we need to + // pass through again to find more text. + $found_keys[] = $key; + + // Locate a space before and after this match, leaving some context on + // each end. + if ($found_position > $context_length) { + $before = strpos($text, ' ', $found_position - $context_length); + if ($before !== FALSE) { + ++$before; + } + } + else { + $before = 0; + } + if ($before !== FALSE && $before <= $found_position) { + if ($text_length > $found_position + $context_length) { + $after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position); + } + else { + $after = $text_length; + } + if ($after !== FALSE && $after > $found_position) { + if ($before < $after) { + // Save this range. + $ranges[$before] = $after; + $length += $after - $before; + } + } + } + } + } + // Next time through this loop, only look for keys we found this time, + // if any. + $remaining_keys = $found_keys; + } + + if (!$ranges) { + // We didn't find any keyword matches, return NULL. + return NULL; + } + + // Sort the text ranges by starting position. + ksort($ranges); + + // Collapse overlapping text ranges into one. The sorting makes it O(n). + $newranges = array(); + $from1 = $to1 = NULL; + foreach ($ranges as $from2 => $to2) { + if ($from1 === NULL) { + // This is the first time through this loop: initialize. + $from1 = $from2; + $to1 = $to2; + continue; + } + if ($from2 <= $to1) { + // The ranges overlap: combine them. + $to1 = max($to1, $to2); + } + else { + // The ranges do not overlap: save the working range and start a new + // one. + $newranges[$from1] = $to1; + $from1 = $from2; + $to1 = $to2; + } + } + // Save the remaining working range. + $newranges[$from1] = $to1; + + // Fetch text within the combined ranges we found. + $out = array(); + foreach ($newranges as $from => $to) { + $out[] = substr($text, $from, $to - $from); + } + if (!$out) { + return NULL; + } + + // Let translators have the ... separator text as one chunk. + $dots = explode('!excerpt', t('... !excerpt ... !excerpt ...')); + + $text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2]; + $text = check_plain($text); + + // Since we stripped the tags at the beginning, highlighting doesn't need to + // handle HTML anymore. + return $this->highlightField($text, $keys, FALSE); + } + + /** + * Marks occurrences of the search keywords in a text field. + * + * @param string $text + * 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, $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) { + $texts[$i] = $this->highlightField($texts[$i], $keys, FALSE); + } + return implode('', $texts); + } + + $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); + // 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); + } + + /** + * Flattens a (possibly multidimensional) array into a string. + * + * @param array $array + * The array to flatten. + * @param string $glue + * (optional) 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); + } + +} diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc b/sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc index f636e2f..0cc4800 100644 --- a/sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc +++ b/sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc @@ -1,5 +1,10 @@ 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)); } } @@ -104,13 +111,18 @@ 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, ); } $text = substr($text, $pos + 1); - 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); if ($m[1]) { // Closing tag. @@ -125,8 +137,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 = ''; diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc b/sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc index fd5d7e9..4a88e73 100644 --- a/sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc +++ b/sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc @@ -1,12 +1,20 @@ 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.)', $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/sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc b/sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc index d8559ca..0091871 100644 --- a/sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc +++ b/sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc @@ -1,10 +1,22 @@ 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( @@ -36,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))); } @@ -50,7 +56,7 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { public function process(&$value) { $stopwords = $this->getStopWords(); - if (empty($stopwords)) { + if (empty($stopwords) || !is_string($value)) { return; } $words = preg_split('/\s+/', $value); @@ -63,17 +69,26 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { $value = implode(' ', $words); } + public function preprocessSearchQuery(SearchApiQuery $query) { + $this->ignored = array(); + parent::preprocessSearchQuery($query); + } + public function postprocessSearchResults(array &$response, SearchApiQuery $query) { - if (isset($this->ignored)) { + if ($this->ignored) { if (isset($response['ignored'])) { $response['ignored'] = array_merge($response['ignored'], $this->ignored); } - else $response['ignored'] = $this->ignored; + else { + $response['ignored'] = $this->ignored; + } } } /** - * @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. */ @@ -91,4 +106,4 @@ class SearchApiStopWords extends SearchApiAbstractProcessor { $this->stopwords = array_flip(array_merge($file_words, $form_words)); return $this->stopwords; } -} \ No newline at end of file +} diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc b/sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc index ba04d1d..14834df 100644 --- a/sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc +++ b/sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc @@ -1,5 +1,10 @@ index->getFields(); + $field_options = array(); + foreach ($fields as $name => $field) { + if (empty($field['real_type']) && search_api_is_text_type($field['type'])) { + $field_options[$name] = $field['name']; + } + } + $form['fields']['#options'] = $field_options; + $form += array( 'spaces' => array( '#type' => 'textfield', @@ -37,7 +53,7 @@ class SearchApiTokenizer extends SearchApiAbstractProcessor { ); if (!empty($this->options)) { - $form['spaces']['#default_value'] = $this->options['spaces']; + $form['spaces']['#default_value'] = $this->options['spaces']; $form['ignorable']['#default_value'] = $this->options['ignorable']; } @@ -76,12 +92,15 @@ class SearchApiTokenizer extends SearchApiAbstractProcessor { } protected function process(&$value) { - $this->prepare(); - if ($this->ignorable) { - $value = preg_replace('/' . $this->ignorable . '+/u', '', $value); - } - if ($this->spaces) { - $value = preg_replace('/' . $this->spaces . '+/u', ' ', $value); + // We don't touch integers, NULL values or the like. + if (is_string($value)) { + $this->prepare(); + if ($this->ignorable) { + $value = preg_replace('/' . $this->ignorable . '+/u', '', $value); + } + if ($this->spaces) { + $value = preg_replace('/' . $this->spaces . '+/u', ' ', $value); + } } } diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_transliteration.inc b/sites/all/modules/contrib/search/search_api/includes/processor_transliteration.inc new file mode 100644 index 0000000..e344886 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/includes/processor_transliteration.inc @@ -0,0 +1,20 @@ +", "<", "<=", ">=", ">". They * have the same semantics as the corresponding SQL operators. @@ -127,31 +146,36 @@ interface SearchApiQueryInterface { public function condition($field, $value, $operator = '='); /** - * Add a sort directive to this search query. If no sort is manually set, the - * results will be sorted descending by relevance. + * Adds a sort directive to this search query. * - * @param $field + * If no sort is manually set, the results will be sorted descending by + * relevance. + * + * @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. - * @param $order + * 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" special field can be used to sort randomly. + * @param string $order * The order to sort items in - either 'ASC' or 'DESC'. * - * @throws SearchApiException - * If the field is multi-valued or of a fulltext type. - * * @return SearchApiQueryInterface * The called object. + * + * @throws SearchApiException + * If the field is multi-valued or of a fulltext type. */ public function sort($field, $order = 'ASC'); /** - * Adds a range of results to return. This will be saved in the query's - * options. If called without parameters, this will remove all range - * restrictions previously set. + * Adds a range of results to return. * - * @param $offset + * This will be saved in the query's options. If called without parameters, + * this will remove all range restrictions previously set. + * + * @param int|null $offset * The zero-based offset of the first result returned. - * @param $limit + * @param int|null $limit * The number of results to return. * * @return SearchApiQueryInterface @@ -166,7 +190,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. @@ -175,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. @@ -193,7 +228,10 @@ 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. */ public function execute(); @@ -202,28 +240,36 @@ 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(); /** - * Postprocess the search results before they are returned. + * Postprocesses the search results before they are returned. * * This method should always be called by execute() and contain all necessary * operations after the results are returned from the server. * * @param array $results - * The results returned by the server, which may be altered. + * The results returned by the server, which may be altered. The data + * structure is the same as returned by execute(). */ public function postExecute(array &$results); /** + * Retrieves the index associated with this search. + * * @return SearchApiIndex * The search index this query should be executed on. */ public function getIndex(); /** - * @return + * Retrieves the search keys for this query. + * + * @return array|string|null * This object's search keys - either a string or an array specifying a * complex search expression. * An array will contain a '#conjunction' key specifying the conjunction @@ -236,41 +282,59 @@ interface SearchApiQueryInterface { * the terms in the array should be excluded; with the "OR" conjunction and * negation, all results containing one or more of the terms in the array * should be excluded. + * + * @see keys() */ public function &getKeys(); /** - * @return + * Retrieves the unparsed search keys for this query as originally entered. + * + * @return array|string|null * The unprocessed search keys, exactly as passed to this object. Has the - * same format as getKeys(). + * same format as the return value of getKeys(). + * + * @see keys() */ public function getOriginalKeys(); /** + * Retrieves the fulltext fields that will be searched for the search keys. + * * @return array * An array containing the fields that should be searched for the search * keys. + * + * @see fields() */ public function &getFields(); /** + * Retrieves the filter object associated with this search query. + * * @return SearchApiQueryFilterInterface * This object's associated filter object. */ public function getFilter(); /** + * Retrieves the sorts set for this query. + * * @return array * An array specifying the sort order for this query. Array keys are the * field names in order of importance, the values are the respective order * in which to sort the results according to the field. + * + * @see sort() */ public function &getSort(); /** - * @param $name string + * Retrieves an option set on this search query. + * + * @param string $name * The name of an option. - * @param $default mixed + * @param mixed $default * The value to return if the specified option is not set. * * @return mixed @@ -279,16 +343,24 @@ interface SearchApiQueryInterface { public function getOption($name, $default = NULL); /** + * Sets an option for this search query. + * * @param string $name * The name of an option. * @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); /** + * Retrieves all options set for this search query. + * + * The return value is a reference to the options so they can also be altered + * this way. + * * @return array * An associative array of query options. */ @@ -297,17 +369,26 @@ interface SearchApiQueryInterface { } /** - * Standard implementation of SearchApiQueryInterface. + * Provides a standard implementation of the 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. * @@ -351,44 +432,14 @@ class SearchApiQuery implements SearchApiQueryInterface { protected $options; /** - * Count for providing a unique ID. + * Flag for whether preExecute() was already called for this query. + * + * @var bool */ - protected static $count = 0; + protected $pre_execute = FALSE; /** - * Constructor for SearchApiQuery objects. - * - * @param SearchApiIndex $index - * The index the query should be executed on. - * @param array $options - * Associative array of options configuring this query. Recognized options - * are: - * - conjunction: The type of conjunction to use for this query - either - * 'AND' or 'OR'. 'AND' by default. This only influences the search keys, - * filters will always use 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 - * recognized parse modes. - * - languages: The languages to search for, as an array of language IDs. - * If not specified, all languages will be searched. Language-neutral - * content (LANGUAGE_NONE) is always searched. - * - 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. - * - 'filter class': Can be used to change the SearchApiQueryFilterInterface - * implementation to use. - * - 'search id': A string that will be used as the identifier when storing - * this search in the Search API's static cache. - * - search_api_access_account: The account which will be used for entity - * access checks, if available and enabled for the index. - * - search_api_bypass_access: If set to TRUE, entity access checks will be - * skipped, even if enabled for the index. - * All options are optional. Third-party modules might define and use other - * options not listed here. - * - * @throws SearchApiException - * If a search on that index (or with those options) won't be possible. + * {@inheritdoc} */ public function __construct(SearchApiIndex $index, array $options = array()) { if (empty($index->options['fields'])) { @@ -415,12 +466,7 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * @return array - * An associative array of parse modes recognized by objects of this class. - * The keys are the parse modes' ids, values are associative arrays - * containing the following entries: - * - name: The translated name of the parse mode. - * - description: (optional) A translated text describing the parse mode. + * {@inheritdoc} */ public function parseModes() { $modes['direct'] = array( @@ -434,18 +480,15 @@ 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; } /** - * Parses the keys string according to the $mode parameter. - * - * @return - * The parsed keys. Either a string or an array. + * {@inheritdoc} */ protected function parseKeys($keys, $mode) { if ($keys === NULL || is_array($keys)) { @@ -460,7 +503,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) { @@ -500,28 +543,15 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Method for creating a filter to use with this query object. - * - * @param $conjunction - * The conjunction to use for the filter - either 'AND' or 'OR'. - * - * @return SearchApiQueryFilterInterface - * A filter object that is set to use the specified conjunction. + * {@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); } /** - * Sets the keys to search for. If this method is not called on the query - * before execution, this will be a filter-only query. - * - * @param $keys - * A string with the unparsed search keys, or NULL to use no search keys. - * - * @return SearchApiQuery - * The called object. + * {@inheritdoc} */ public function keys($keys = NULL) { $this->orig_keys = $keys; @@ -534,17 +564,7 @@ class SearchApiQuery implements SearchApiQueryInterface { return $this; } /** - * Sets the fields that will be searched for the search keys. If this is not - * called, all fulltext fields should be searched. - * - * @param array $fields - * An array containing fulltext fields that should be searched. - * - * @throws SearchApiException - * If one of the fields isn't of type "text". - * - * @return SearchApiQueryInterface - * The called object. + * {@inheritdoc} */ public function fields(array $fields) { $fulltext_fields = $this->index->getFulltextFields(); @@ -556,13 +576,7 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Adds a subfilter to this query's filter. - * - * @param SearchApiQueryFilterInterface $filter - * A SearchApiQueryFilter object that should be added as a subfilter. - * - * @return SearchApiQuery - * The called object. + * {@inheritdoc} */ public function filter(SearchApiQueryFilterInterface $filter) { $this->filter->filter($filter); @@ -570,24 +584,7 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Add a new ($field $operator $value) condition filter. - * - * @param $field - * The field to filter on, e.g. 'title'. - * @param $value - * The value the field should have (or be related to by the operator). - * @param $operator - * The operator to use for checking the constraint. The following operators - * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They - * have the same semantics as the corresponding SQL operators. - * If $field is a fulltext field, $operator can only be "=" or "<>", which - * are in this case interpreted as "contains" or "doesn't contain", - * respectively. - * If $value is NULL, $operator also can only be "=" or "<>", meaning the - * field must have no or some value, respectively. - * - * @return SearchApiQuery - * The called object. + * {@inheritdoc} */ public function condition($field, $value, $operator = '=') { $this->filter->condition($field, $value, $operator); @@ -595,20 +592,7 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Add a sort directive to this search query. If no sort is manually set, the - * results will be sorted descending by relevance. - * - * @param $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. - * @param $order - * The order to sort items in - either 'ASC' or 'DESC'. - * - * @throws SearchApiException - * If the field is multi-valued or of a fulltext type. - * - * @return SearchApiQuery - * The called object. + * {@inheritdoc} */ public function sort($field, $order = 'ASC') { $fields = $this->index->options['fields']; @@ -616,6 +600,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))); } @@ -629,17 +617,7 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Adds a range of results to return. This will be saved in the query's - * options. If called without parameters, this will remove all range - * restrictions previously set. - * - * @param $offset - * The zero-based offset of the first result returned. - * @param $limit - * The number of results to return. - * - * @return SearchApiQueryInterface - * The called object. + * {@inheritdoc} */ public function range($offset = NULL, $limit = NULL) { $this->options['offset'] = $offset; @@ -649,42 +627,9 @@ class SearchApiQuery implements SearchApiQueryInterface { /** - * Executes this search query. - * - * @return array - * 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. - * - 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. - * - score: A float measuring how well the item fits the search. - * - fields: (optional) If set, an array containing some field values - * 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. - * - 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. - * - excerpt: (optional) If set, an HTML text containing highlighted - * portions of the fulltext that match the query. - * - warnings: A numeric array of translated warning messages that may be - * displayed to the user. - * - ignored: A numeric array of search keys that were ignored for this - * search (e.g., because of being too short or stop words). - * - performance: An associative array with the time taken (as floats, in - * seconds) for specific parts of the search execution: - * - complete: The complete runtime of the query. - * - hooks: Hook invocations and other client-side preprocessing. - * - preprocessing: Preprocessing of the service class. - * - 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. + * {@inheritdoc} */ - public final function execute() { + public function execute() { $start = microtime(TRUE); // Prepare the query for execution by the server. @@ -711,7 +656,15 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Helper method for adding a language filter. + * Adds language filters for the query. + * + * Internal helper function. + * + * @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) { @@ -755,124 +708,92 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * Prepares the query object for the search. - * - * This method should always be called by execute() and contain all necessary - * operations before the query is passed to the server's search() method. + * {@inheritdoc} */ public function preExecute() { - // Add filter for languages. - if (isset($this->options['languages'])) { - $this->addLanguages($this->options['languages']); + // Make sure to only execute this once per query. + if (!$this->pre_execute) { + $this->pre_execute = TRUE; + // Add filter for languages. + if (isset($this->options['languages'])) { + $this->addLanguages($this->options['languages']); + } + + // Add fulltext fields, unless set + if ($this->fields === NULL) { + $this->fields = $this->index->getFulltextFields(); + } + + // Preprocess query. + $this->index->preprocessSearchQuery($this); + + // Let modules alter the query. + drupal_alter('search_api_query', $this); } - - // Add fulltext fields, unless set - if ($this->fields === NULL) { - $this->fields = $this->index->getFulltextFields(); - } - - // Preprocess query. - $this->index->preprocessSearchQuery($this); - - // Let modules alter the query. - drupal_alter('search_api_query', $this); } /** - * Postprocess the search results before they are returned. - * - * This method should always be called by execute() and contain all necessary - * operations after the results are returned from the server. - * - * @param array $results - * The results returned by the server, which may be altered. + * {@inheritdoc} */ public function postExecute(array &$results) { // Postprocess results. $this->index->postprocessSearchResults($results, $this); + + // Let modules alter the results. + drupal_alter('search_api_results', $results, $this); } /** - * @return SearchApiIndex - * The search index this query will be executed on. + * {@inheritdoc} */ public function getIndex() { return $this->index; } /** - * @return - * This object's search keys - either a string or an array specifying a - * complex search expression. - * An array will contain a '#conjunction' key specifying the conjunction - * type, and search strings or nested expression arrays at numeric keys. - * Additionally, a '#negation' key might be present, which means – unless it - * maps to a FALSE value – that the search keys contained in that array - * should be negated, i.e. not be present in returned results. The negation - * works on the whole array, not on each contained term individually – i.e., - * with the "AND" conjunction and negation, only results that contain all - * the terms in the array should be excluded; with the "OR" conjunction and - * negation, all results containing one or more of the terms in the array - * should be excluded. + * {@inheritdoc} */ public function &getKeys() { return $this->keys; } /** - * @return - * The unprocessed search keys, exactly as passed to this object. Has the - * same format as getKeys(). + * {@inheritdoc} */ public function getOriginalKeys() { return $this->orig_keys; } /** - * @return array - * An array containing the fields that should be searched for the search - * keys. + * {@inheritdoc} */ public function &getFields() { return $this->fields; } /** - * @return SearchApiQueryFilterInterface - * This object's associated filter object. + * {@inheritdoc} */ public function getFilter() { return $this->filter; } /** - * @return array - * An array specifying the sort order for this query. Array keys are the - * field names in order of importance, the values are the respective order - * in which to sort the results according to the field. + * {@inheritdoc} */ public function &getSort() { return $this->sort; } /** - * @param $name string - * The name of an option. - * - * @return mixed - * The option with the specified name, if set, or NULL otherwise. + * {@inheritdoc} */ public function getOption($name, $default = NULL) { return array_key_exists($name, $this->options) ? $this->options[$name] : $default; } /** - * @param string $name - * The name of an option. - * @param mixed $value - * The new value of the option. - * - * @return The option's previous value. + * {@inheritdoc} */ public function setOption($name, $value) { $old = $this->getOption($name); @@ -881,36 +802,113 @@ class SearchApiQuery implements SearchApiQueryInterface { } /** - * @return array - * An associative array of query options. + * {@inheritdoc} */ public function &getOptions() { return $this->options; } + /** + * Implements the magic __sleep() method to avoid serializing the index. + */ + public function __sleep() { + $this->index_id = $this->index->machine_name; + $keys = get_object_vars($this); + unset($keys['index']); + return array_keys($keys); + } + + /** + * Implements the magic __wakeup() method to reload the query's index. + */ + public function __wakeup() { + if (!isset($this->index) && !empty($this->index_id)) { + $this->index = search_api_index_load($this->index_id); + unset($this->index_id); + } + } + + /** + * Implements the magic __clone() method to clone the filter, too. + */ + public function __clone() { + $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"; + } + $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; + } + } /** - * Interface representing a search query filter, that filters on one or more - * fields with a specific conjunction (AND or OR). + * Represents a filter on a search query. * - * Methods not noting otherwise will return the object itself, so calls can be - * chained. + * Filters apply conditions on one or more fields with a specific conjunction + * (AND or OR) and may contain nested filters. */ interface SearchApiQueryFilterInterface { /** * Constructs a new filter that uses the specified conjunction. * - * @param $conjunction - * The conjunction to use for this filter - either 'AND' or 'OR'. + * @param string $conjunction + * (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. * - * @param $conjunction + * @param string $conjunction * The conjunction to use for this filter - either 'AND' or 'OR'. * * @return SearchApiQueryFilterInterface @@ -921,7 +919,7 @@ interface SearchApiQueryFilterInterface { /** * Adds a subfilter. * - * @param $filter + * @param SearchApiQueryFilterInterface $filter * A SearchApiQueryFilterInterface object that should be added as a * subfilter. * @@ -931,13 +929,13 @@ interface SearchApiQueryFilterInterface { public function filter(SearchApiQueryFilterInterface $filter); /** - * Add a new ($field $operator $value) condition. + * Adds a new ($field $operator $value) condition. * - * @param $field + * @param string $field * The field to filter on, e.g. 'title'. - * @param $value + * @param mixed $value * The value the field should have (or be related to by the operator). - * @param $operator + * @param string $operator * The operator to use for checking the constraint. The following operators * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They * have the same semantics as the corresponding SQL operators. @@ -953,53 +951,79 @@ interface SearchApiQueryFilterInterface { public function condition($field, $value, $operator = '='); /** - * @return + * Retrieves the conjunction used by this filter. + * + * @return string * The conjunction used by this filter - either 'AND' or 'OR'. */ public function getConjunction(); /** + * 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(); + /** + * 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(); + } /** - * Standard implementation of SearchApiQueryFilterInterface. + * Provides a standard implementation of SearchApiQueryFilterInterface. */ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { /** - * Array containing subfilters. Each of these is either an array - * (field, value, operator), or another SearchApiFilter object. + * Array containing subfilters. + * + * Each of these is either an array (field, value, operator), or another + * SearchApiFilter object. + * + * @var array */ protected $filters; - /** String specifying this filter's conjunction ('AND' or 'OR'). */ + /** + * String specifying this filter's conjunction ('AND' or 'OR'). + * + * @var string + */ protected $conjunction; /** - * Constructs a new filter that uses the specified conjunction. - * - * @param $conjunction - * The conjunction to use for this filter - either 'AND' or 'OR'. + * {@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); } /** - * Sets this filter's conjunction. - * - * @param $conjunction - * The conjunction to use for this filter - either 'AND' or 'OR'. - * - * @return SearchApiQueryFilter - * The called object. + * {@inheritdoc} */ public function setConjunction($conjunction) { $this->conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND'; @@ -1007,14 +1031,7 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { } /** - * Adds a subfilter. - * - * @param $filter - * A SearchApiQueryFilterInterface object that should be added as a - * subfilter. - * - * @return SearchApiQueryFilter - * The called object. + * {@inheritdoc} */ public function filter(SearchApiQueryFilterInterface $filter) { $this->filters[] = $filter; @@ -1022,24 +1039,7 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { } /** - * Add a new ($field $operator $value) condition. - * - * @param $field - * The field to filter on, e.g. 'title'. - * @param $value - * The value the field should have (or be related to by the operator). - * @param $operator - * The operator to use for checking the constraint. The following operators - * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They - * have the same semantics as the corresponding SQL operators. - * If $field is a fulltext field, $operator can only be "=" or "<>", which - * are in this case interpreted as "contains" or "doesn't contain", - * respectively. - * If $value is NULL, $operator also can only be "=" or "<>", meaning the - * field must have no or some value, respectively. - * - * @return SearchApiQueryFilter - * The called object. + * {@inheritdoc} */ public function condition($field, $value, $operator = '=') { $this->filters[] = array($field, $value, $operator); @@ -1047,20 +1047,66 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface { } /** - * @return - * The conjunction used by this filter - either 'AND' or 'OR'. + * {@inheritdoc} */ public function getConjunction() { return $this->conjunction; } /** - * @return array - * An array containing this filter's subfilters. Each of these is either an - * array (field, value, operator), or another SearchApiFilter object. + * {@inheritdoc} */ public function &getFilters() { return $this->filters; } + /** + * {@inheritdoc} + */ + public function hasTag($tag) { + return isset($this->tags[$tag]); + } + + /** + * {@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; + } + + /** + * 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; + } + } + } + + /** + * 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) : ''; + } + } diff --git a/sites/all/modules/contrib/search/search_api/includes/server_entity.inc b/sites/all/modules/contrib/search/search_api/includes/server_entity.inc index 0436171..6ab8bd7 100644 --- a/sites/all/modules/contrib/search/search_api/includes/server_entity.inc +++ b/sites/all/modules/contrib/search/search_api/includes/server_entity.inc @@ -1,5 +1,10 @@ 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 occurs, + * 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 occurs, + * 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 occurs, + * 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 occurs, + * 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); } + /** + * 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/sites/all/modules/contrib/search/search_api/includes/service.inc b/sites/all/modules/contrib/search/search_api/includes/service.inc index 29fc637..046e5d3 100644 --- a/sites/all/modules/contrib/search/search_api/includes/service.inc +++ b/sites/all/modules/contrib/search/search_api/includes/service.inc @@ -1,16 +1,27 @@ - * 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. * - * @return + * The server's $original property can be used to inspect the old property + * values. + * + * @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 @@ -110,33 +136,39 @@ 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 indexed 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. * * @param SearchApiIndex $index * The updated index. * - * @return + * @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 @@ -144,14 +176,20 @@ interface SearchApiServiceInterface { * * If the index wasn't added to the server, the method call should be ignored. * + * Implementations of this method should also check whether $index->read_only + * is set, and don't delete any indexed data if it is. + * * @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. @@ -182,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 @@ -195,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. @@ -234,6 +275,10 @@ interface SearchApiServiceInterface { /** * Abstract class with generic implementation of most service methods. + * + * For creating your own service class extending this class, you only need to + * implement indexItems(), deleteItems() and search() from the + * SearchApiServiceInterface interface. */ abstract class SearchApiAbstractService implements SearchApiServiceInterface { @@ -250,13 +295,9 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { protected $options = array(); /** - * Constructor for a service class, setting the server configuration used with - * this service. + * Implements SearchApiServiceInterface::__construct(). * * The default implementation sets $this->server and $this->options. - * - * @param SearchApiServer $server - * The server object for this service. */ public function __construct(SearchApiServer $server) { $this->server = $server; @@ -264,46 +305,28 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { } /** - * Form callback. Might be called on an uninitialized object - in this case, - * the form is for configuring a newly created server. + * Implements SearchApiServiceInterface::__construct(). * * Returns an empty form by default. - * - * @return array - * A form array for setting service-specific options. */ public function configurationForm(array $form, array &$form_state) { return array(); } /** - * Validation callback for the form returned by configurationForm(). + * Implements SearchApiServiceInterface::__construct(). * * Does nothing by default. - * - * @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) { return; } /** - * Submit callback for the form returned by configurationForm(). + * Implements SearchApiServiceInterface::__construct(). * * The default implementation just ensures that additional elements in * $options, not present in the form, don't get lost at the update. - * - * @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) { if (!empty($this->options)) { @@ -313,32 +336,16 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { } /** - * Determines whether this service class implementation supports a given - * feature. Features are optional extensions to Search API functionality and - * usually defined and used by third-party modules. + * Implements SearchApiServiceInterface::__construct(). * - * There are currently three features defined directly in the Search API - * project: - * - "search_api_facets", by the search_api_facetapi module. - * - "search_api_facets_operator_or", also by the search_api_facetapi module. - * - "search_api_mlt", by the search_api_views module. - * Other contrib modules might define additional features. These should always - * be properly documented in the module by which they are defined. - * - * @param string $feature - * The name of the optional feature. - * - * @return boolean - * TRUE if this service knows and supports the specified feature. FALSE - * otherwise. + * The default implementation always returns FALSE. */ public function supportsFeature($feature) { return FALSE; } /** - * View this server's settings. Output can be HTML or a render array, a
- * listing all relevant settings is preferred. + * Implements SearchApiServiceInterface::__construct(). * * The default implementation does a crude output as a definition list, with * option names taken from the configuration form. @@ -364,8 +371,31 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { } /** - * Called once, when the server is first created. Allows it to set up its - * necessary infrastructure. + * 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(). * * Does nothing, by default. */ @@ -374,93 +404,67 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface { } /** - * 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. + * Implements SearchApiServiceInterface::__construct(). * - * @return - * TRUE, if the update requires reindexing of all content on the server. + * The default implementation always returns FALSE. */ public function postUpdate() { return FALSE; } /** - * Notifies this server that it is about to be deleted from the database and - * should therefore clean up, if appropriate. - * - * 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 - * present in the database anymore at this point. + * Implements SearchApiServiceInterface::__construct(). * * By default, deletes all indexes from this server. */ 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); + } } } /** - * Add a new index to this server. + * Implements SearchApiServiceInterface::__construct(). * * Does nothing, by default. - * - * @param SearchApiIndex $index - * The index to add. */ public function addIndex(SearchApiIndex $index) { return; } /** - * Notify the server that the indexed 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. + * Implements SearchApiServiceInterface::__construct(). * - * @param SearchApiIndex $index - * The updated index. - * - * @return - * TRUE, if this change affected the server in any way that forces it to - * re-index the content. FALSE otherwise. + * The default implementation always returns FALSE. */ public function fieldsUpdated(SearchApiIndex $index) { return FALSE; } /** - * Remove 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 - * $index->server. + * Implements SearchApiServiceInterface::__construct(). * * By default, removes all items from that index. - * - * @param $index - * Either an object representing the index to remove, or its machine name - * (if the index was completely deleted). */ public function removeIndex($index) { - $this->deleteItems('all', $index); + if (is_object($index) && empty($index->read_only)) { + $this->deleteItems('all', $index); + } } /** - * Create a query object for searching on an index lying on this server. + * Implements SearchApiServiceInterface::__construct(). * - * @param SearchApiIndex $index - * The index to search on. - * @param $options - * Associative array of options configuring this query. See - * SearchApiQueryInterface::__construct(). - * - * @return SearchApiQueryInterface - * An object for searching the given index. - * - * @throws SearchApiException - * If the server is currently disabled. + * The default implementation returns a SearchApiQuery object. */ public function query(SearchApiIndex $index, $options = array()) { return new SearchApiQuery($index, $options); diff --git a/sites/all/modules/contrib/search/search_api/search_api.admin.css b/sites/all/modules/contrib/search/search_api/search_api.admin.css index 0c49e05..b82798d 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.admin.css +++ b/sites/all/modules/contrib/search/search_api/search_api.admin.css @@ -1,44 +1,229 @@ +/** + * @file + * Styles for Search API admin pages. + */ -td.search-api-status { +/* + * OVERVIEW + */ + +.search-api-overview td.search-api-status { text-align: center; } -div.search-api-edit-menu { +.search-api-overview td { + 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.) + */ + +/** + * 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; - 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; } -div.search-api-edit-menu ul { - margin: 0 0.5em; - padding: 0; -} - -div.search-api-edit-menu ul li { - padding: 0; +/* 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; } +.dropbutton-multiple.open .dropbutton .secondary-action { + display: block; +} + +.dropbutton-toggle { + bottom: 0; + display: block; + 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; } - -/* Workaround for http://drupal.org/node/1015798 */ -.vertical-tabs fieldset div.fieldset-wrapper fieldset legend { - display: block; - margin-bottom: 2em; -} - diff --git a/sites/all/modules/contrib/search/search_api/search_api.admin.inc b/sites/all/modules/contrib/search/search_api/search_api.admin.inc index 5fbc8d8..89680db 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.admin.inc +++ b/sites/all/modules/contrib/search/search_api/search_api.admin.inc @@ -1,7 +1,14 @@ server][$index->machine_name] = $index; @@ -46,15 +54,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 +67,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); - $rows[] = $row; + $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[] = _search_api_deep_copy($row); + if (!empty($indexes[$server->machine_name])) { foreach ($indexes[$server->machine_name] as $index) { $url = $pre_index . '/' . $index->machine_name; @@ -76,18 +90,20 @@ 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); - $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); - $rows[] = $row; + $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[] = _search_api_deep_copy($row); } } } @@ -101,13 +117,9 @@ 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); - $rows[] = $row; + $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name)); + $row[] = theme('search_api_dropbutton', array('links' => $links)); + $rows[] = _search_api_deep_copy($row); } } @@ -118,32 +130,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 +
+
"; } /** @@ -231,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'), @@ -240,15 +268,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. */ @@ -274,12 +303,13 @@ 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); + } } /** - * 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); @@ -308,19 +338,15 @@ function search_api_admin_add_server_submit(array $form, array &$form_state) { } /** - * Title callback for viewing or editing a server or index. - */ -function search_api_admin_item_title($object) { - return $object->name; -} - -/** - * Displays a server's details. + * Page callback: Displays information about a server. * * @param SearchApiServer $server * The server to display. - * @param $action - * One of 'enable', 'disable', 'delete'; or NULL if the server is only viewed. + * @param string|null $action + * (optional) An action to execute for the server. One of 'enable', 'disable' + * or 'clear'. + * + * @see search_api_menu() */ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) { if (!empty($action)) { @@ -340,7 +366,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; } } @@ -349,22 +375,41 @@ 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, '#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, '#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; } /** - * Theme function for displaying a server. + * Returns HTML for displaying a server. * * @param array $variables * An associative array containing: @@ -373,69 +418,178 @@ 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 + * 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 array of additional server information in the format specified + * by SearchApiAbstractService::getExtraInformation(). + * + * @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_id = $variables['class_id']; + $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($class_id); + $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 . '

'; + if ($options) { + $label = t('Service options'); + $info = render($options); + $rows[] = _search_api_deep_copy($row); + } + + 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 = ''; + } + + 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 .= '
' . "\n"; } - if (!empty($options)) { - $output .= '
' . t('Service options') . '
' . "\n"; - $output .= '
' . "\n"; - $output .= render($options); - $output .= '
' . "\n"; - } - - $output .= '
' . t('Configuration status') . '
' . "\n"; - $output .= '
' . "\n"; - $output .= theme('entity_status', array('status' => $status)); - $output .= '
' . "\n"; - - $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; } /** - * Edit a server's settings. + * Form constructor for server operations. + * + * @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'), + '#submit' => array('search_api_server_status_form_clear_submit') + ); + + $count = $server->enabled ? search_api_server_tasks_count($server) : 0; + if ($count) { + $message = format_plural($count, '@count pending task must be executed before indexing.', '@count pending tasks must be executed before indexing.'); + drupal_set_message($message, 'warning', FALSE); + $form['execute_pending_tasks'] = array( + '#type' => 'submit', + '#value' => t('Execute all pending tasks on this server'), + '#submit' => array('search_api_server_status_form_execute_pending_tasks_submit') + ); + } + + return $form; +} + +/** + * Form submission handler for search_api_server_status_form(). + * + * Used for the "Execute all pending tasks" button. + */ +function search_api_server_status_form_execute_pending_tasks_submit($form, &$form_state) { + $server_id = $form_state['server']->machine_name; + $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/execute-tasks"; +} + +/** + * Form submission handler for search_api_server_status_form(). + * + * Used for the "Delete all indexed data" button. + */ +function search_api_server_status_form_clear_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. * * @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; @@ -473,23 +627,36 @@ 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; } /** - * 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); + 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); + } } /** - * 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); @@ -507,89 +674,154 @@ function search_api_admin_server_edit_submit(array $form, array &$form_state) { } /** - * Form callback showing a form for adding an index. + * 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 constructor for adding an index. + * + * @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); - // List enabled servers first. - foreach ($servers as $server) { - if ($server->enabled) { - $form['server']['#options'][$server->machine_name] = $server->name; + elseif (!$old_type) { + $old_type = $form_state['step_one']['item_type']; + } + + 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'); } } - 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['datasource']['#prefix'] = '
'; + $form['datasource']['#suffix'] = '
'; + + $form['old_type'] = array( + '#type' => 'value', + '#value' => $old_type, ); - $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['datasource_config'] = array( + '#type' => 'value', + '#value' => !empty($datasource_form), ); $form['submit'] = array( @@ -601,31 +833,70 @@ function search_api_admin_add_index(array $form, array &$form_state) { } /** - * Validation callback for search_api_admin_add_index. + * 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); } /** - * 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); - $values = $form_state['values']; - // Validation of whether the server of an enabled index is also enabled is - // done in the *_insert() function. + 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); drupal_set_message(t('The index was successfully created. Please set up its indexed fields now.')); @@ -633,16 +904,17 @@ 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. 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 = 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)) { @@ -660,32 +932,54 @@ function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NUL } else { $ret = drupal_get_form('search_api_admin_confirm', 'index', $action, $index); - if ($ret) { + if (!empty($ret['actions'])) { return $ret; } } } - $ret = array( + $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, '#name' => $index->name, '#machine_name' => $index->machine_name, '#description' => $index->description, '#item_type' => $index->item_type, + '#datasource_config' => $index->datasource()->getConfigurationSummary($index), '#enabled' => $index->enabled, - '#server' => $index->server(), + '#server' => $server, '#options' => $index->options, '#fields' => $index->getFields(), + '#indexed_items' => $status['indexed'], + '#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); + } return $ret; } /** - * Theme function for displaying an index. + * Returns HTML for a search index. * * @param array $variables * An associative array containing: @@ -694,305 +988,320 @@ function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NUL * - 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. * - 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. 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.). * - read_only: Boolean indicating whether this index is read only. + * + * @return string + * HTML for a search index. + * + * @ingroup themeable */ function theme_search_api_index(array $variables) { - extract($variables); + $machine_name = $variables['machine_name']; + $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']; + $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 = ''; - $output .= '

' . check_plain($name) . '

' . "\n"; - - $output .= '
' . "\n"; - - $output .= '
' . t('Status') . '
' . "\n"; - $output .= '
'; - if ($enabled) { - $output .= t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $machine_name . '/disable'))); + if ($description) { + $output .= '

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

'; } - 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)))))); + + // 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) { + $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $machine_name . '/disable'))); + $class = 'ok'; + } + 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 { - $output .= t('disabled'); + $info = t('disabled'); } - $output .= '
' . "\n"; + $label = t('Status'); + $rows[] = _search_api_deep_copy($row); + $class = ''; - $output .= '
' . t('Machine name') . '
' . "\n"; - $output .= '
' . check_plain($machine_name) . '
' . "\n"; - - $output .= '
' . t('Item type') . '
' . "\n"; + $label = t('Item type'); $type = search_api_get_item_type_info($item_type); - $type = $type['name']; - $output .= '
' . check_plain($type) . '
' . "\n"; + $item_type = !empty($type['name']) ? $type['name'] : $item_type; + $info = check_plain($item_type); + $rows[] = _search_api_deep_copy($row); - if (!empty($description)) { - $output .= '
' . t('Description') . '
' . "\n"; - $output .= '
' . nl2br(check_plain($description)) . '
' . "\n"; + if ($datasource_config) { + $label = t('Item type configuration'); + $info = check_plain($datasource_config); + $rows[] = _search_api_deep_copy($row); } - if (!empty($server)) { - $output .= '
' . t('Server') . '
' . "\n"; - $output .= '
' . l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name); - if (!empty($server->description)) { - $output .= '

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

'; - } - $output .= '
' . "\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); } - 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"; + 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 { - $output .= '
' . format_plural($options['cron_limit'], '1 item per cron batch.', '@count items per cron batch.') . '
' . "\n"; + $class = 'warning'; + $info = t('No items will be indexed during cron runs.'); } + $label = t('Cron batch size'); + $rows[] = _search_api_deep_copy($row); - 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"; - } + $theme = array( + '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) . '
'; + + 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'; } - - $output .= '
' . "\n"; - } - elseif ($read_only) { - $output .= '
' . t('Read only') . '
' . "\n"; - $output .= '
' . t('This index is read-only.') . '
' . "\n"; + 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); } - $output .= '
' . t('Configuration status') . '
' . "\n"; - $output .= '
' . "\n"; - $output .= theme('entity_status', array('status' => $status)); - $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); + } - $output .= '
'; + $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 function for displaying an index status form. + * 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) { - $enabled = !empty($index->enabled); - $status = search_api_index_status($index); - $server = $index->server(); - +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['status_message'] = array( - '#type' => 'item', - '#title' => t('Status'), - '#description' => $enabled ? t('The index is currently enabled.') : t('The index is currently disabled.'), + $form['index'] = array( + '#type' => 'fieldset', + '#title' => t('Index now'), ); - if (!empty($server->enabled)) { - $form['status'] = array( - '#type' => 'submit', - '#value' => $enabled ? t('Disable') : t('Enable'), - ); + $form['index']['#attributes']['class'][] = 'container-inline'; + + $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, + ); + + // 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; } - 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.'), - ); + $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, + ); - return $form; - } - - 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).', - array('@total' => $status['total'])); - } - elseif (!$status['indexed']) { - $form['progress']['#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).', - array('@indexed' => $status['indexed'], '@total' => $status['total'], '@percentage' => $percentage)); - } - - if (!$all) { - $form['index'] = array( - '#type' => 'fieldset', - '#title' => t('Index now'), - '#collapsible' => TRUE, - ); - $form['index']['settings'] = array( - '#type' => 'fieldset', - '#title' => t('Advanced settings'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form['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']['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']['button'] = array( - '#type' => 'submit', - '#value' => t('Index now'), - ); - $form['index']['total'] = array( - '#type' => 'value', - '#value' => $status['total'], - ); - $form['index']['remaining'] = array( - '#type' => 'value', - '#value' => $status['total'] - $status['indexed'], - ); - } - } - - 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( - '#type' => 'fieldset', - '#title' => t('Clear index'), - '#collapsible' => TRUE, - ); - $form['clear']['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.'), - ); - $form['clear']['button'] = array( - '#type' => 'submit', - '#value' => t('Clear index'), - ); - } + $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; } /** - * 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']) { - 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; + } } } /** - * 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']; $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 = $pre . '/enable'; - 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 . '/status'; - 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.')); - } - else { - drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error'); - } - $redirect = $pre . '/status'; break; - default: - throw new SearchApiException(t('Unknown action.')); + case t('Queue all items for reindexing'): + $form_state['redirect'] .= '/reindex'; + break; + + case t('Clear all indexed data'): + $form_state['redirect'] .= '/clear'; + break; } } /** - * 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_validate() + * @see search_api_admin_index_edit_submit() */ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) { $form_state['index'] = $index; @@ -1006,12 +1315,21 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#default_value' => $index->name, '#required' => TRUE, ); + try { + $enabled_fixed = !$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), + // Can't enable an index that's not lying on any server. + '#disabled' => $enabled_fixed, ); $form['description'] = array( '#type' => 'textarea', @@ -1025,18 +1343,20 @@ 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; - } + $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)); - } + + $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'), @@ -1067,22 +1387,48 @@ 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; } /** - * Submit callback for search_api_admin_index_edit. + * 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); @@ -1096,13 +1442,17 @@ 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. + * + * @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(); @@ -1240,10 +1590,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( @@ -1342,7 +1695,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. @@ -1359,16 +1714,18 @@ 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']; 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; - $fields_set = !empty($options['fields']); // Store callback and processor settings. foreach ($form_state['callbacks'] as $name => $callback) { @@ -1386,6 +1743,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(); @@ -1394,7 +1752,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']))) { @@ -1411,16 +1770,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]); - } - } - - } } } @@ -1434,26 +1783,26 @@ 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(); - 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.')); + $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'; } /** * 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; @@ -1465,10 +1814,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); @@ -1477,10 +1827,17 @@ 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')); + $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. + 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'; @@ -1488,17 +1845,22 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp $form['description'] = array( '#type' => 'item', '#title' => t('Select fields to index'), - '#description' => t('

The datatype of a field determines how it can be used for searching and filtering. ' . + '#description' => t('

The datatype of a field determines how it can be used for searching and filtering. Fields indexed with type "Fulltext" and multi-valued fields (marked with 1) cannot be used for sorting. ' . 'The boost is used to give additional weight to certain fields, e.g. titles or tags. It only takes effect for fulltext fields.

' . '

Whether detailed field types are supported depends on the type of server this index resides on. ' . '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']); + if (search_api_is_list_type($info['type'])) { + $form['fields'][$key]['title']['#markup'] .= ' 1'; + $multi_valued_field_present = TRUE; + } + $form['fields'][$key]['machine_name']['#markup'] = check_plain($key); if (isset($info['description'])) { $form['fields'][$key]['description'] = array( '#type' => 'value', @@ -1510,17 +1872,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( @@ -1540,10 +1905,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. @@ -1587,12 +1961,17 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp } } + if (!empty($multi_valued_field_present)) { + $form['note']['#markup'] = '
1 ' . t('Multi-valued field') . '
'; + } + $form['submit'] = array( '#type' => 'submit', '#value' => t('Save changes'), ); if ($additional) { + asort($additional); reset($additional); $form['additional'] = array( '#type' => 'fieldset', @@ -1634,14 +2013,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(); @@ -1667,7 +2049,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,10 +2125,13 @@ 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']; - $header = array(t('Field'), t('Indexed'), t('Type'), t('Boost')); + $header = array(t('Field'), t('Machine name'), t('Indexed'), t('Type'), t('Boost')); $rows = array(); foreach (element_children($form['fields']) as $name) { @@ -1756,7 +2142,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( @@ -1766,11 +2152,13 @@ function theme_search_api_admin_fields_table($variables) { } } + $note = isset($form['note']) ? $form['note'] : ''; $submit = $form['submit']; $additional = isset($form['additional']) ? $form['additional'] : FALSE; - unset($form['submit'], $form['additional']); + unset($form['note'], $form['submit'], $form['additional']); $output = drupal_render_children($form); $output .= theme('table', array('header' => $header, 'rows' => $rows)); + $output .= render($note); $output .= render($submit); if ($additional) { $output .= render($additional); @@ -1780,11 +2168,12 @@ 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']; $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(); @@ -1814,18 +2203,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 index workflow.')); - $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow'; + drupal_set_message(t('Please set up the indexing workflow.')); + $form_state['redirect'] = $index_path . '/workflow'; } return; } @@ -1840,14 +2229,23 @@ 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'; } - /** - * Helper function for displaying a generic confirmation form. + * Form constructor for 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. 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. + * + * @return array|false * Either a form array, or FALSE if this combination of type and action is * not supported. */ @@ -1855,15 +2253,24 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio switch ($type) { case 'server': switch ($action) { + 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': $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; + case 'delete': if ($entity->hasStatus(ENTITY_OVERRIDDEN)) { $text = array( @@ -1883,12 +2290,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)), @@ -1897,6 +2323,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( @@ -1916,6 +2343,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio ); } break; + default: return FALSE; } @@ -1956,8 +2384,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/sites/all/modules/contrib/search/search_api/search_api.admin.js b/sites/all/modules/contrib/search/search_api/search_api.admin.js index d841ec1..9ff40ae 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.admin.js +++ b/sites/all/modules/contrib/search/search_api/search_api.admin.js @@ -1,7 +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 () { @@ -43,19 +50,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/sites/all/modules/contrib/search/search_api/search_api.api.php b/sites/all/modules/contrib/search/search_api/search_api.api.php index f7506f3..d1740c6 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.api.php +++ b/sites/all/modules/contrib/search/search_api/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: @@ -24,9 +22,12 @@ * - 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. + * + * @see hook_search_api_service_info_alter() */ function hook_search_api_service_info() { $services['example_some'] = array( @@ -49,13 +50,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) { @@ -95,6 +97,9 @@ function hook_search_api_service_info_alter(array &$service_info) { * - datasource controller: A class implementing the * SearchApiDataSourceControllerInterface interface which will be used as * the data source controller for this type. + * - entity_type: (optional) If the type represents entities, the entity type. + * This is used by SearchApiAbstractDataSourceController for determining the + * entity type of items. Other datasource controllers might ignore this. * Other, datasource-specific settings might also be placed here. These should * be specified with the data source controller in question. * @@ -109,6 +114,7 @@ function hook_search_api_item_type_info() { $types[$type] = array( 'name' => $info['label'], 'datasource controller' => 'SearchApiEntityDataSourceController', + 'entity_type' => $type, ); } } @@ -121,7 +127,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. @@ -186,6 +193,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. @@ -220,6 +229,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 @@ -255,6 +279,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. * @@ -278,15 +316,53 @@ function hook_search_api_index_items_alter(array &$items, SearchApiIndex $index) example_store_indexed_entity_ids($index->item_type, array_keys($items)); } +/** + * Allows modules to react after items were indexed. + * + * @param SearchApiIndex $index + * The used index. + * @param array $item_ids + * An array containing the indexed items' IDs. + */ +function hook_search_api_items_indexed(SearchApiIndex $index, array $item_ids) { + if ($index->getEntityType() == 'node') { + // Flush page cache of the search page. + cache_clear_all(url('search'), 'cache_page'); + } +} + /** * 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) { - $info = entity_get_info($index->item_type); - $query->condition($info['entity keys']['id'], 0, '!='); + // 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, '<>'); + } +} + +/** + * 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); + } } /** @@ -508,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/sites/all/modules/contrib/search/search_api/search_api.drush.inc b/sites/all/modules/contrib/search/search_api/search_api.drush.inc index 6652554..8c995b1 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.drush.inc +++ b/sites/all/modules/contrib/search/search_api/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( @@ -45,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."), ), @@ -68,13 +95,25 @@ function search_api_drush_command() { 'aliases' => array('sapi-r'), ); + $items['search-api-reindex-items'] = array( + 'description' => 'Force re-indexing of one or more specific items.', + 'examples' => array( + 'drush search-api-reindex-items node 12,34,56' => dt('Schedule the nodes with ID 12, 34 and 56 for re-indexing.'), + ), + 'arguments' => array( + 'entity_type' => dt('The entity type whose items should be re-indexed.'), + 'entities' => dt('The entities of the given entity type to be re-indexed.'), + ), + 'aliases' => array('sapi-ri'), + ); + $items['search-api-clear'] = array( 'description' => 'Clear one or all search indexes and mark them for re-indexing.', '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.'), @@ -82,6 +121,65 @@ function search_api_drush_command() { 'aliases' => array('sapi-c'), ); + $items['search-api-execute-tasks'] = array( + 'description' => 'Execute all pending tasks or all for a given server.', + 'examples' => array( + 'drush search-api-execute-tasks my_solr_server' => dt('Execute all pending tasks on !server', array('!server' => 'my_solr_server')), + 'drush sapi-et my_solr_server' => dt('Execute all pending tasks on !server', array('!server' => 'my_solr_server')), + 'drush sapi-et' => dt('Execute all pending tasks on all servers.') + ), + 'arguments' => array( + 'server_id' => dt('The numeric ID or machine name of a server to execute tasks on.'), + ), + 'aliases' => array('sapi-et') + ); + + $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'), + ); + + $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; } @@ -111,15 +209,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; @@ -127,6 +231,79 @@ 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) { + $vars = array('!index' => $index->name); + if (!$index->enabled) { + 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'); + } + } + 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.", $vars), '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) { + $vars = array('!index' => $index->name); + if ($index->enabled) { + $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'); + } + if (!$success) { + drush_log(dt("Error disabling index !index.", $vars), 'error'); + } + } + else { + drush_log(dt("The index !index is already disabled.", $vars), 'error'); + } + } +} + /** * Display index status. */ @@ -175,40 +352,77 @@ 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; } + + $process_batch = FALSE; foreach ($indexes as $index) { - // Get the number of remaing items to index. - $datasource = $index->datasource(); - $index_status = $datasource->getIndexStatus($index); - $remaining = $index_status['total'] - $index_status['indexed']; - - // 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; - } - - // Get the number 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'); - } - else { - // Launch the batch process. - drush_backend_batch_process(); + if (_drush_search_api_batch_indexing_create($index, $limit, $batch_size)) { + $process_batch = TRUE; } } + + if ($process_batch) { + drush_backend_batch_process(); + } +} + +/** + * 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; + } + + // 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; + } + + // 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; } /** @@ -259,6 +473,33 @@ function drush_search_api_reindex($index_id = NULL) { } } +/** + * Marks the given entities as needing to be re-indexed. + */ +function drush_search_api_reindex_items($entity_type, $entities) { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + + // Validate list of entity ids. + if (!empty($entities) && !preg_match('#^[0-9]*(,[0-9]*)*$#', $entities)) { + drush_log(dt('Entities should be a single numeric entity ID or a list with the numeric entity IDs separated by comma.'), 'error'); + return; + } + + $ids = explode(',', $entities); + + if (!empty($ids)) { + search_api_track_item_change($entity_type, $ids); + + $combined_ids = array(); + foreach ($ids as $id) { + $combined_ids[] = $entity_type . '/' . $id; + } + search_api_track_item_change('multiple', $combined_ids); + } +} + /** * Clear an index. */ @@ -278,12 +519,82 @@ function drush_search_api_clear($index_id = NULL) { } /** - * Helper function to return an index or all indexes as an array. + * Execute all pending tasks or all for a given server. + */ +function drush_search_api_execute_tasks($server_id = NULL) { + if (search_api_drush_static(__FUNCTION__)) { + return; + } + + // Attempt to load the associated server. + $server = NULL; + if ($server_id) { + $servers = search_api_drush_get_server($server_id); + if (!$servers) { + return; + } + $server = reset($servers); + } + + // Process batch op with drush. + try { + search_api_execute_pending_tasks($server); + drush_log(dt('!server tasks have been successfully executed.', array('!server' => $server->machine_name ? $server->machine_name : 'All')), 'ok'); + } + catch (SearchApiException $e) { + drush_log($e->getMessage(), 'error'); + } +} + +/** + * 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) { @@ -298,7 +609,35 @@ 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.')); + drush_print(); + drush_search_api_server_list(); + } + 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 */ @@ -308,4 +647,77 @@ function search_api_drush_static($function) { return TRUE; } $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.')); + } } diff --git a/sites/all/modules/contrib/search/search_api/search_api.info b/sites/all/modules/contrib/search/search_api/search_api.info index 2f7b357..b59bc92 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.info +++ b/sites/all/modules/contrib/search/search_api/search_api.info @@ -1,6 +1,6 @@ name = Search API -description = "Provides a generic API for modules offering search capabilites." -dependencies[] = entity +description = "Provides a generic API for modules offering search capabilities." +dependencies[] = entity:entity core = 7.x package = Search @@ -11,28 +11,35 @@ 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 +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 files[] = includes/datasource_external.inc +files[] = includes/datasource_multiple.inc files[] = includes/exception.inc files[] = includes/index_entity.inc 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 files[] = includes/query.inc files[] = includes/server_entity.inc files[] = includes/service.inc configure = admin/config/search/search_api -; Information added by drupal.org packaging script on 2013-01-09 -version = "7.x-1.4" +; Information added by Drupal.org packaging script on 2019-03-11 +version = "7.x-1.26" core = "7.x" project = "search_api" -datestamp = "1357726719" - +datestamp = "1552334832" diff --git a/sites/all/modules/contrib/search/search_api/search_api.install b/sites/all/modules/contrib/search/search_api/search_api.install index 8fbb0e4..61f686d 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.install +++ b/sites/all/modules/contrib/search/search_api/search_api.install @@ -191,9 +191,124 @@ 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( + '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; } +/** + * Implements hook_requirements(). + */ +function search_api_requirements($phase) { + $requirements = array(); + + if ($phase == 'runtime') { + // Check whether at least one server has pending tasks. + if (search_api_server_tasks_count()) { + $items = array(); + + $conditions = array('enabled' => TRUE); + foreach (search_api_server_load_multiple(FALSE, $conditions) as $server) { + $count = search_api_server_tasks_count($server); + if ($count) { + $args = array( + '@name' => $server->name, + ); + $text = format_plural($count, '@name has @count pending task.', '@name has @count pending tasks.', $args); + $items[] = l($text, "admin/config/search/search_api/server/{$server->machine_name}/execute-tasks"); + } + } + + if ($items) { + $text = t('There are pending tasks for the following servers:'); + $text .= theme('item_list', array( + 'type' => 'ul', + 'items' => $items, + )); + if (count($items) > 1) { + $label = t('Execute pending tasks on all servers'); + $text .= l($label, 'admin/config/search/search_api/execute-tasks'); + } + $requirements['search_api_pending_tasks'] = array( + 'title' => t('Search API'), + 'value' => $text, + 'severity' => REQUIREMENT_WARNING, + ); + } + } + } + + return $requirements; +} + /** * Implements hook_install(). * @@ -214,6 +329,7 @@ function search_api_install() { 'server' => NULL, 'item_type' => 'node', 'options' => array( + 'index_directly' => 1, 'cron_limit' => '50', 'data_alter_callbacks' => array( 'search_api_alter_node_access' => array( @@ -291,7 +407,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.')); } /** @@ -307,8 +423,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); + } } } @@ -321,8 +442,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. + } } } @@ -330,7 +456,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'); } @@ -605,7 +730,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'; } @@ -806,3 +931,189 @@ 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(); +} + +/** + * 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) { + $task += array( + 'index_id' => NULL, + 'data' => NULL, + ); + $insert->values($task); + } + $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(); + } +} + +/** + * 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( + '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/sites/all/modules/contrib/search/search_api/search_api.module b/sites/all/modules/contrib/search/search_api/search_api.module index bba0681..c1cf4e4 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.module +++ b/sites/all/modules/contrib/search/search_api/search_api.module @@ -1,5 +1,10 @@ 'View', - 'weight' => -10, 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, ); $items[$pre . '/server/%search_api_server/edit'] = array( 'title' => 'Edit', @@ -65,6 +70,28 @@ 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/execute-tasks'] = array( + 'title' => 'Execute pending tasks', + 'description' => 'Attempt to process pending tasks for a given server.', + 'page callback' => 'search_api_execute_pending_tasks', + 'page arguments' => array(5), + 'access callback' => 'search_api_access_execute_tasks_batch', + 'access arguments' => array(5), + 'type' => MENU_CALLBACK, + ); + $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', @@ -77,6 +104,15 @@ function search_api_menu() { 'access arguments' => array(5), 'file' => 'search_api.admin.inc', 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 10, + ); + $items[$pre . '/execute-tasks'] = array( + 'title' => 'Execute pending tasks', + 'description' => 'Attempt to process pending server tasks.', + 'page callback' => 'search_api_execute_pending_tasks', + 'access callback' => 'search_api_access_execute_tasks_batch', + 'type' => MENU_LOCAL_ACTION, ); $items[$pre . '/index/%search_api_index'] = array( 'title' => 'View index', @@ -93,27 +129,16 @@ function search_api_menu() { '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, - ); $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), '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', @@ -122,20 +147,32 @@ 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', - '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'), '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', @@ -148,15 +185,86 @@ function search_api_menu() { 'access arguments' => array(5), 'file' => 'search_api.admin.inc', 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 10, ); return $items; } +/** + * Implements hook_help(). + */ +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.') . '

    '; + } +} + +/** + * Implements hook_hook_info(). + */ +function search_api_hook_info() { + // We use the same group for all hooks, so save code lines. + $hook_info = array( + 'group' => 'search_api', + ); + return array( + 'search_api_service_info' => $hook_info, + 'search_api_service_info_alter' => $hook_info, + 'search_api_item_type_info' => $hook_info, + 'search_api_item_type_info_alter' => $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, + 'search_api_server_delete' => $hook_info, + 'default_search_api_server' => $hook_info, + 'default_search_api_server_alter' => $hook_info, + 'search_api_index_load' => $hook_info, + 'search_api_index_insert' => $hook_info, + 'search_api_index_update' => $hook_info, + 'search_api_index_reindex' => $hook_info, + 'search_api_index_delete' => $hook_info, + 'default_search_api_index' => $hook_info, + 'default_search_api_index_alter' => $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, @@ -164,10 +272,13 @@ function search_api_theme() { 'machine_name' => '', 'description' => NULL, 'enabled' => NULL, + 'class_id' => NULL, 'class_name' => NULL, 'class_description' => NULL, + 'indexes' => array(), 'options' => array(), 'status' => ENTITY_CUSTOM, + 'extra' => array(), ), 'file' => 'search_api.admin.inc', ); @@ -178,11 +289,13 @@ function search_api_theme() { 'machine_name' => '', 'description' => NULL, 'item_type' => NULL, + 'datasource_config' => NULL, 'enabled' => NULL, 'server' => NULL, 'options' => array(), 'fields' => array(), 'indexed_items' => 0, + 'on_server' => NULL, 'total_items' => 0, 'status' => ENTITY_CUSTOM, 'read_only' => 0, @@ -216,51 +329,78 @@ function search_api_permission() { /** * Implements hook_cron(). * - * Will index $options['cron-limit'] items for each enabled index. + * This will first execute any pending server tasks. After that, items will + * be indexed on all enabled indexes with a non-zero cron limit. Indexing will + * run for the time set in the search_api_index_worker_callback_runtime variable + * (defaulting to 15 seconds), but will at least index one batch of items on + * each index. + * + * @see search_api_server_tasks_check() */ 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']) + // Execute pending server tasks. + search_api_server_tasks_check(); + + // 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); - $ids = search_api_get_items_to_index($index, -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; + $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); } - // 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(). */ @@ -272,6 +412,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( @@ -287,6 +428,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( @@ -345,6 +487,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( @@ -406,6 +561,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; @@ -417,6 +585,15 @@ function search_api_entity_property_info() { * Calls the postCreate() method for the server. */ function search_api_search_api_server_insert(SearchApiServer $server) { + // Check whether this is actually part of a revert. + $reverts = &drupal_static('search_api_search_api_server_delete', array()); + if (isset($reverts[$server->machine_name])) { + $server->original = $reverts[$server->machine_name]; + unset($reverts[$server->machine_name]); + search_api_search_api_server_update($server); + unset($server->original); + return; + } $server->postCreate(); } @@ -432,51 +609,13 @@ function search_api_search_api_server_update(SearchApiServer $server) { $index->reindex(); } } - if ($server->enabled != $server->original->enabled) { + 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)); } } } @@ -488,19 +627,19 @@ function search_api_search_api_server_update(SearchApiServer $server) { * Calls the preDelete() method for the server. */ function search_api_search_api_server_delete(SearchApiServer $server) { - $server->preDelete(); - - // Only react on real delete, not revert. - if (!$server->hasStatus(ENTITY_IN_CODE)) { - foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) { - $index->update(array('server' => NULL, 'enabled' => FALSE)); - } + if ($server->hasStatus(ENTITY_IN_CODE)) { + $reverts = &drupal_static(__FUNCTION__, array()); + $reverts[$server->machine_name] = $server; + return; } - $tasks = variable_get('search_api_tasks', array()); - unset($tasks[$server->machine_name]); - variable_set('search_api_tasks', $tasks); + $server->preDelete(); + foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) { + $index->update(array('server' => NULL, 'enabled' => FALSE)); + } + + search_api_server_tasks_delete(NULL, $server); } /** @@ -510,6 +649,16 @@ function search_api_search_api_server_delete(SearchApiServer $server) { * the index is enabled). */ function search_api_search_api_index_insert(SearchApiIndex $index) { + // Check whether this is actually part of a revert. + $reverts = &drupal_static('search_api_search_api_index_delete', array()); + if (isset($reverts[$index->machine_name])) { + $index->original = $reverts[$index->machine_name]; + unset($reverts[$index->machine_name]); + search_api_search_api_index_update($index); + unset($index->original); + return; + } + $index->postCreate(); } @@ -517,6 +666,10 @@ 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 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. if ($index->server != $index->original->server) { @@ -525,34 +678,29 @@ 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) { + try { + $new_server = $index->server(TRUE); + // If the server is enabled, we call addIndex(); otherwise, we save the task. $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); + 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 + // We also have to re-index all content. _search_api_index_reindex($index); } @@ -563,8 +711,8 @@ function search_api_search_api_index_update(SearchApiIndex $index) { $new_fields = $index->options + array('fields' => array()); $new_fields = $new_fields['fields']; if ($old_fields != $new_fields) { - if ($index->server && $index->server()->fieldsUpdated($index)) { - _search_api_index_reindex($index); + if ($index->server) { + $index->server()->fieldsUpdated($index); } } @@ -586,15 +734,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); - } } /** @@ -603,6 +742,13 @@ function search_api_search_api_index_update(SearchApiIndex $index) { * Removes all data for indexes not available any more. */ function search_api_search_api_index_delete(SearchApiIndex $index) { + // Only react on real delete, not revert. + if ($index->hasStatus(ENTITY_IN_CODE)) { + $reverts = &drupal_static(__FUNCTION__, array()); + $reverts[$index->machine_name] = $index; + return; + } + cache_clear_all($index->getCacheId(''), 'cache', TRUE); $index->postDelete(); } @@ -611,7 +757,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'; @@ -642,16 +788,82 @@ 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' || !module_exists($file->name)) { + 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) { + $url = url("admin/config/search/search_api/index/$id"); + $links[] = '' . check_plain($name) . ''; + } + + $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) { + $url = url("admin/config/search/search_api/server/$id"); + $links[] = '' . check_plain($name) . ''; + } + + $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(). * - * 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 @@ -665,18 +877,21 @@ 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)); } } /** * 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. + * 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) { // We only react on entity operations for types with property information, as @@ -684,21 +899,31 @@ 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 (!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); + } + } + if (isset($id)) { search_api_track_item_change($type, array($id)); + $combined_id = $type . '/' . $id; + search_api_track_item_change('multiple', array($combined_id)); } } /** * 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 @@ -709,9 +934,89 @@ 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)); } } +/** + * 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); + 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)) { + $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(). + * + * 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(). + * + * Recalculates fields settings if the cardinality of the field has changed from + * or to 1. + */ +function search_api_field_update_field($field, $prior_field) { + $before = $prior_field['cardinality']; + $after = $field['cardinality']; + if ($before != $after && ($before == 1 || $after == 1)) { + // Unfortunately, we cannot call this right away since the field information + // is only stored after the hook is called. + drupal_register_shutdown_function('search_api_index_recalculate_fields'); + } +} + +/** + * Implements hook_flush_caches(). + * + * Recalculates fields settings in case the schema (in most cases: the + * multiplicity) of a property has changed. + */ +function search_api_flush_caches() { + search_api_index_recalculate_fields(); +} + /** * Implements hook_search_api_item_type_info(). * @@ -720,34 +1025,62 @@ function search_api_entity_delete($entity, $type) { 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', - ); - } + 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; } +/** + * 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(). */ -function search_api_modules_enabled(array $modules) { - // New modules might offer additional entity types, invalidating the cached - // item type information. +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'); + 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. +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'); + drupal_static_reset('search_api_get_service_info'); } /** @@ -761,6 +1094,13 @@ function search_api_search_api_alter_callback_info() { // Filters should be executed first. 'weight' => -10, ); + $callbacks['search_api_alter_role_filter'] = array( + 'name' => t('Role filter'), + 'description' => t('Exclude users from indexing based on their role.'), + 'class' => 'SearchApiAlterRoleFilter', + // Filters should be executed first. + 'weight' => -10, + ); $callbacks['search_api_alter_add_url'] = array( 'name' => t('URL field'), 'description' => t("Adds the item's URL to the indexed data."), @@ -788,14 +1128,29 @@ 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. 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', ); + $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.'), + 'class' => 'SearchApiAlterUserStatus', + ); return $callbacks; } @@ -806,7 +1161,7 @@ function search_api_search_api_alter_callback_info() { function search_api_search_api_processor_info() { $processors['search_api_case_ignore'] = array( 'name' => t('Ignore case'), - 'description' => t('This processor will make searches case-insensitive for all fulltext fields (and, optionally, also for filters on string fields).'), + 'description' => t('This processor will make searches case-insensitive for fulltext or string fields.'), 'class' => 'SearchApiIgnoreCase', ); $processors['search_api_html_filter'] = array( @@ -817,6 +1172,14 @@ function search_api_search_api_processor_info() { 'class' => 'SearchApiHtmlFilter', 'weight' => 10, ); + if (module_exists('transliteration')) { + $processors['search_api_transliteration'] = array( + 'name' => t('Transliteration'), + 'description' => t('This processor will make searches insensitive to accents and other non-ASCII characters.'), + 'class' => 'SearchApiTransliteration', + 'weight' => 15, + ); + } $processors['search_api_tokenizer'] = array( 'name' => t('Tokenizer'), 'description' => t('Tokenizes fulltext data by stripping whitespace. ' . @@ -832,6 +1195,18 @@ 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' => 40, + ); return $processors; } @@ -839,9 +1214,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) { @@ -855,11 +1230,21 @@ function search_api_track_item_insert($type, array $item_ids) { return; } - search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes); + try { + $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; + 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'])) { - $indexed = search_api_index_specific_items_delayed($index, $item_ids); + search_api_index_specific_items_delayed($index, $item_ids); } } } @@ -885,19 +1270,42 @@ 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); + 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)) { + $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 + // 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); + } } /** @@ -907,9 +1315,20 @@ 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); + try { + $index->datasource()->trackItemQueued($item_ids, $index); + } + catch (SearchApiException $e) { + watchdog_exception('search_api', $e); + } } /** @@ -922,6 +1341,7 @@ function search_api_track_item_queued(SearchApiIndex $index, array $item_ids) { */ function search_api_track_item_indexed(SearchApiIndex $index, array $item_ids) { $index->datasource()->trackItemIndexed($item_ids, $index); + module_invoke_all('search_api_items_indexed', $index, $item_ids); } /** @@ -941,77 +1361,393 @@ 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 { + $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; + 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(); - if ($server->enabled) { + $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); } - else { - $tasks = variable_get('search_api_tasks', array()); - foreach ($item_ids as $id) { - $tasks[$server->machine_name][$index->machine_name][] = 'delete-' . $id; - } - variable_set('search_api_tasks', $tasks); - } + } + 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); } } } /** - * Indexes items for the specified index. Only items marked as changed are - * indexed, in their order of change (if known). + * 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'); + // 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(); + foreach ($tasks as $task) { + if (!$server || $server->machine_name != $task->server_id) { + $server = search_api_server_load($task->server_id); + if (!$server) { + continue; + } + } + switch ($task->type) { + case 'addIndex': + $index = search_api_index_load($task->index_id); + if ($index) { + $server->addIndex($index); + } + 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 2; + } + $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 + // (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; +} + +/** + * Provides a batch wrapper for search_api_server_tasks_check(). + * + * @param SearchApiServer|null $server + * (optional) The server whose tasks should be executed, or NULL to execute + * tasks for all servers. + */ +function search_api_execute_pending_tasks(SearchApiServer $server = NULL) { + batch_set(array( + 'title' => t('Processing pending tasks'), + 'operations' => array( + array( + 'search_api_execute_pending_tasks_batch', + array( + $server, + ), + ), + ), + 'finished' => 'search_api_execute_pending_tasks_finished' + )); + if ($server) { + $path = 'admin/config/search/search_api/server/' . $server->machine_name; + } + else { + $path = 'admin/config/search/search_api'; + } + + if (function_exists('drush_backend_batch_process')) { + drush_backend_batch_process(); + } + else { + batch_process($path); + } +} + +/** + * Executes pending server tasks as part of a batch operation. + */ +function search_api_execute_pending_tasks_batch(SearchApiServer $server = NULL, &$context) { + if (!isset($context['results']['total'])) { + $context['results']['total'] = search_api_server_tasks_count($server); + } + $total = $context['results']['total']; + + search_api_server_tasks_check($server); + + $remaining = search_api_server_tasks_count($server); + $executed = max($total - $remaining, 0); + + $args['@remaining'] = $remaining; + $context['message'] = format_plural($executed, 'Successfully executed @count task, @remaining remaining.', 'Successfully executed @count tasks, @remaining remaining.', $args); + $context['finished'] = $executed / $total; +} + +/** + * Batch finish callback for pending server tasks. + */ +function search_api_execute_pending_tasks_finished($success, $results, $operations) { + if ($success) { + // Clear the previous warning. + drupal_get_messages('warning'); + + // Alert user to the number of tasks executed. + drupal_set_message(format_plural($results['total'], 'Successfully executed @count task.', 'Successfully executed @count tasks.')); + } +} + +/** + * Return the number of pending tasks. + * + * @param SearchApiServer|null $server + * (optional) The server for which tasks should be counted, or NULL to count + * for all enabled servers. + * + * @return int + * The number of pending tasks for the server, or in total. + */ +function search_api_server_tasks_count(SearchApiServer $server = NULL) { + $query = db_select('search_api_task', 't') + ->fields('t'); + + if ($server) { + $query->condition('server_id', $server->machine_name); + } + else { + $query->join('search_api_server', 's', 's.machine_name = t.server_id'); + $query->condition('s.enabled', 1); + } + + return $query->countQuery()->execute()->fetchField(); +} + +/** + * Access callback: Checks whether a user can execute pending tasks. + * + * @param SearchApiServer|null $server + * (optional) The server for which tasks would be executed. + */ +function search_api_access_execute_tasks_batch(SearchApiServer $server = NULL) { + return user_access('administer search_api') + && search_api_server_tasks_count($server) + && (!$server || $server->enabled); +} + +/** + * 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(); +} + +/** + * Recalculates the saved fields of an index. + * + * This is mostly necessary when the multiplicity of the underlying properties + * change. The method will re-examine the data structure of the entities in each + * 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 SearchApiIndex[]|false $indexes + * An array of SearchApiIndex objects on which to perform the operation, or + * FALSE to perform it on all indexes. + */ +function search_api_index_recalculate_fields($indexes = FALSE) { + if (!is_array($indexes)) { + $indexes = search_api_index_load_multiple(FALSE); + } + $stored_keys = drupal_map_assoc(array('type', 'entity_type', 'real_type', 'boost')); + foreach ($indexes as $index) { + if (empty($index->options['fields'])) { + continue; + } + // We have to clear the cache, both static and stored, before using + // getFields(). Otherwise, we'd just use the stale data which the fields + // options are probably already based on. + cache_clear_all($index->getCacheId() . '-1-0', 'cache'); + $index->resetCaches(); + // getFields() automatically uses the actual data types to correct possible + // stale data. + $fields = $index->getFields(); + foreach ($fields as $key => $field) { + $fields[$key] = array_intersect_key($field, $stored_keys); + if (isset($fields[$key]['boost']) && $fields[$key]['boost'] == '1.0') { + unset($fields[$key]['boost']); + } + } + // Use a more accurate method of determining if the fields settings are + // equal to avoid needlessly re-indexing the whole index. + if ($fields != $index->options['fields']) { + $options = $index->options; + $options['fields'] = $fields; + $index->update(array('options' => $options)); + } + } +} + +/** + * Test two setting arrays (or individual settings) for equality. + * + * @param mixed $setting1 + * The first setting (array). + * @param mixed $setting2 + * The second setting (array). + * + * @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)) { + return $setting1 == $setting2; + } + foreach ($setting1 as $key => $value) { + if (!array_key_exists($key, $setting2)) { + return FALSE; + } + if (!_search_api_settings_equals($value, $setting2[$key])) { + return FALSE; + } + unset($setting2[$key]); + } + // If any keys weren't unset previously, they are not present in $setting1 and + // the two are different. + return !$setting2; +} + +/** + * 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; } /** @@ -1024,18 +1760,36 @@ 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) { + // 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(); foreach ($items as $id => $item) { - $cloned_items[$id] = clone $item; + if (is_object($item)) { + $cloned_items[$id] = clone $item; + } + else { + // Normally, items that can't be loaded shouldn't be returned by + // entity_load (and other loadItems() implementations). Therefore, this is + // an extremely rare case, which seems to happen during installation for + // 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); + } } $indexed = $items ? $index->index($cloned_items) : array(); if ($indexed) { @@ -1114,25 +1868,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); @@ -1143,9 +1886,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 @@ -1259,22 +2003,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)) { @@ -1286,15 +2056,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() */ @@ -1302,8 +2072,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)) { @@ -1353,10 +2149,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; @@ -1376,10 +2175,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; @@ -1394,39 +2196,75 @@ 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"]['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); } } 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); + $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); } } } /** - * 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 comments', $account))) { // Simple hack for returning no results. $query->condition('status', 0); $query->condition('status', 1); @@ -1434,43 +2272,45 @@ 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); } /** - * 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 + * @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() */ function search_api_is_text_type($type, array $allowed = array('text')) { return array_search(search_api_extract_inner_type($type), $allowed) !== FALSE; @@ -1483,7 +2323,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) { @@ -1496,7 +2336,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. */ @@ -1523,7 +2363,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) { @@ -1540,7 +2380,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. @@ -1553,33 +2393,66 @@ function search_api_extract_inner_type($type) { } /** - * Utility function for extracting specific fields from an EntityMetadataWrapper - * object. + * Helper function for reacting to index updates with regards to the datasource. + * + * When an overridden index is reverted, its numerical ID will sometimes change. + * Since the default datasource implementation uses that for referencing + * indexes, the index ID in the items table must be updated accordingly. This is + * implemented in this function. + * + * Modules implementing other datasource controllers, that use a table other + * than {search_api_item}, can use this function, too. It should be called + * 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 + * called) if the indexes are referenced by numerical ID in the items table. + * + * @param SearchApiIndex $index + * The index that was changed. + * @param string $table + * The table containing items information, analogous to {search_api_item}. + * @param string $column + * The column in $table that holds the index's numerical ID. + */ +function search_api_index_update_datasource(SearchApiIndex $index, $table, $column = 'index_id') { + if ($index->id != $index->original->id) { + db_update($table) + ->fields(array($column => $index->id)) + ->condition($column, $index->original->id) + ->execute(); + } +} + +/** + * 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()) { + $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'])) { - 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'])) { @@ -1605,26 +2478,40 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields // Set "defaults" in case an error occurs later. $info['value'] = NULL; $info['original_type'] = $info['type']; - 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')); + if (isset($wrapper->$field)) { + try { + // Set the original type according to the field wrapper's info. + $property_info = $wrapper->$field->info(); + $info['original_type'] = $property_info['type']; + + // 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); + } + } } - $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. - $t = search_api_extract_inner_type($property_info['type']); - if (isset($entity_infos[$t])) { - // 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; + catch (EntityMetadataWrapperException $e) { + // This might happen for entity-typed properties that are NULL, e.g., + // for comments without parent. } } - catch (EntityMetadataWrapperException $e) { - // This might happen for entity-typed properties that are NULL, e.g., - // for comments without parent. - } } else { list($prefix, $key) = explode(':', $field, 2); @@ -1641,7 +2528,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']; } @@ -1712,19 +2599,20 @@ function search_api_server_load($id, $reset = FALSE) { * * @see entity_load() * - * @param $ids + * @param array|false $ids * An array of server IDs or machine names, or FALSE to load all servers. - * @param $conditions + * @param array $conditions * An array of conditions on the {search_api_server} table in the form * 'field' => $value. - * @param $reset + * @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) { - return entity_load_multiple_by_name('search_api_server', $ids, $conditions, $reset); + $servers = entity_load('search_api_server', $ids, $conditions, $reset); + return entity_key_array_by_property($servers, 'machine_name'); } /** @@ -1737,6 +2625,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. @@ -1751,6 +2646,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. @@ -1758,13 +2666,22 @@ function search_api_title_delete_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) { 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. * @@ -1784,13 +2701,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. */ @@ -1801,13 +2718,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) { @@ -1817,12 +2735,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) { @@ -1831,13 +2751,37 @@ 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. * * @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) { @@ -1854,12 +2798,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); } /** @@ -1868,22 +2812,23 @@ function search_api_index_load($id, $reset = FALSE) { * * @see entity_load() * - * @param $ids + * @param array|false $ids * An array of index IDs or machine names, or FALSE to load all indexes. - * @param $conditions + * @param array $conditions * An array of conditions on the {search_api_index} table in the form * 'field' => $value. - * @param $reset + * @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) { // This line is a workaround for a weird PDO bug in PHP 5.2. // See http://drupal.org/node/889286. new SearchApiIndex(); - return entity_load_multiple_by_name('search_api_index', $ids, $conditions, $reset); + $indexes = entity_load('search_api_index', $ids, $conditions, $reset); + return entity_key_array_by_property($indexes, 'machine_name'); } /** @@ -1912,14 +2857,43 @@ function search_api_index_url(SearchApiIndex $index) { } /** - * Property callback. + * Returns an index's server. * - * @return SearchApiServer - * The server this index currently resides on, or NULL if the index - * is currently unassigned. + * 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|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; + } +} + +/** + * 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'), + ); } /** @@ -1941,12 +2915,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. */ @@ -1959,12 +2933,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. */ @@ -1979,14 +2953,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); @@ -1997,11 +2971,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); @@ -2015,7 +2992,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) { @@ -2031,42 +3008,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); - } } /** @@ -2075,7 +3016,7 @@ function _search_api_empty_cron_queue(SearchApiIndex $index, $mark_changed = FAL * @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) { @@ -2089,7 +3030,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) { @@ -2101,6 +3042,65 @@ 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 ($field_value === NULL) { + return $field_value; + } + 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. * @@ -2119,39 +3119,63 @@ function search_api_index_options_list() { } /** - * Cron queue worker callback for indexing some items. + * Options list callback for entity types. * - * @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. + * Will only include entity types which specify entity property information. * - * @return - * The number of successfully indexed items. + * @return string[] + * An array of entity type machine names mapped to their human-readable + * names. */ -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; +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']; } } - catch (SearchApiException $e) { - watchdog_exception('search_api', $e); + 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; } /** @@ -2211,18 +3235,117 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ } /** - * Create and set a batch for indexing items. + * 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. + * + * @throws SearchApiException + * If an error prevented the search from completing. + */ +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; +} + +/** + * 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. * * @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) { @@ -2260,10 +3383,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; @@ -2282,8 +3405,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) { @@ -2311,13 +3444,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 information 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/sites/all/modules/contrib/search/search_api/search_api.rules.inc b/sites/all/modules/contrib/search/search_api/search_api.rules.inc index 575d28c..0725f4c 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.rules.inc +++ b/sites/all/modules/contrib/search/search_api/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', @@ -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()); @@ -60,8 +63,8 @@ function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiI return; } - if ($index) { + $type = $index->item_type; $indexes = array($index); } else { @@ -77,7 +80,7 @@ function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiI } if ($index_immediately) { foreach ($indexes as $index) { - $indexed = search_api_index_specific_items_delayed($index, $item_ids); + search_api_index_specific_items_delayed($index, $item_ids); } } else { diff --git a/sites/all/modules/contrib/search/search_api/search_api.test b/sites/all/modules/contrib/search/search_api/search_api.test index e65cc7a..76a8970 100644 --- a/sites/all/modules/contrib/search/search_api/search_api.test +++ b/sites/all/modules/contrib/search/search_api/search_api.test @@ -1,29 +1,66 @@ assertResponse(200, t('HTTP code 200 returned.')); + $this->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, t('HTTP code 200 returned.')); + $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,81 +69,100 @@ 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() { + module_enable(array('search_api_test_2')); $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(); $this->deleteServer(); + $this->disableModules(); } - protected function deleteDefaultIndex() { - $this->drupalPost('admin/config/search/search_api/index/default_node_index/delete', array(), t('Confirm')); + /** + * 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); } - protected function insertItems($offset = 0) { + /** + * 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. + * + * @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, t('@count items inserted.', array('@count' => $count))); + $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.'), t('"No servers" message is displayed.')); - } - + /** + * Creates a test index via the UI and tests whether this works correctly. + */ protected function createIndex() { $values = array( 'name' => '', @@ -132,23 +188,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.')); - $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->assertTrue($found, 'Correct redirect.'); + $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.'); + $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 +233,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,18 +265,18 @@ 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 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', 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->drupalGet("admin/config/search/search_api/index/$id/status"); - $this->assertText(t('The index is currently disabled.'), t('"Disabled" status displayed.')); + $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(t('disabled'), '"Disabled" status displayed.'); } + /** + * Creates a test server via the UI and tests whether this works correctly. + */ protected function createServer() { $values = array( 'name' => '', @@ -249,100 +305,375 @@ 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.')); - $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->assertTrue($found, 'Correct redirect.'); + $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.'); + $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 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', 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.'); } + /** + * Moves the index onto the server and enables it. + */ protected function enableIndex() { $values = array( 'server' => $this->server_id, ); $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->drupalGet("admin/config/search/search_api/index/{$this->index_id}/enable"); + $this->clickLink(t('enable')); $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', t('No search results returned without indexing.')); - $this->assertText('results = ()', t('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. + $this->index(); + $this->server(); + + $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->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->checkIndexStatus(); // 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, ); $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->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, ); $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); + // 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, ); $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->checkIndexStatus(10); + + // Reset the static cache for the server. + $this->server(); } + /** + * 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. + * @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, $on_server = NULL) { + $url = "admin/config/search/search_api/index/{$this->index_id}"; + if (strpos($this->url, $url) === FALSE) { + $this->drupalGet($url); + } + + $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; + } + $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; + } + + $this->assertText(t('enabled'), '"Enabled" status displayed.'); + if ($indexed == $total) { + $this->assertRaw('disabled="disabled"', '"Index now" form disabled.'); + } + else { + $this->assertNoRaw('disabled="disabled"', '"Index now" form enabled.'); + } + } + + /** + * 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', 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.')); + $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.'); - $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.')); + $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.'); } + /** + * 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. + $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.'); + + // 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, 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. + $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, 1); + + $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 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. + */ protected function editServer() { $values = array( 'name' => 'test-name-foo', @@ -351,22 +682,74 @@ 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.'); } + /** + * 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(), 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' => 10)), t('!field displayed.', array('!field' => t('Correct index status')))); + $this->assertText(t('@indexed/@total indexed', array('@indexed' => 0, '@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. + * 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', t('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->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.'); + } + + /** + * 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_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'), '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, '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.'); + $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->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.'); } } @@ -379,8 +762,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); @@ -390,6 +784,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; @@ -405,6 +813,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', @@ -413,11 +827,15 @@ class SearchApiUnitTest extends DrupalWebTestCase { ); } + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp('entity', 'search_api'); $this->index = entity_create('search_api_index', array( 'id' => 1, 'name' => 'test', + 'machine_name' => 'test', 'enabled' => 1, 'item_type' => 'user', 'options' => array( @@ -436,55 +854,63 @@ 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(); $this->checkTokenizer(); $this->checkHtmlFilter(); + $this->checkEntityDatasource(); } - 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']; - $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() { - $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( @@ -524,33 +950,36 @@ 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() { + /** + * Tests the functionality of the "Tokenizer" processor. + */ + protected function checkTokenizer() { $orig = 'Foo bar1 BaZ, La-la-la.'; $processed1 = array( array( @@ -614,28 +1043,31 @@ 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() { + /** + * Tests the functionality of the "HTML filter" processor. + */ + 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;". @@ -644,6 +1076,7 @@ END; $tags = << 'This', 'score' => 1), @@ -651,6 +1084,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), @@ -694,7 +1128,78 @@ 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.'); + } + + /** + * 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(); } } diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test.info b/sites/all/modules/contrib/search/search_api/tests/search_api_test.info index f6d0706..2c5f46e 100644 --- a/sites/all/modules/contrib/search/search_api/tests/search_api_test.info +++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test.info @@ -1,18 +1,16 @@ - -name = Search API test +name = Search API Test description = "Some dummy implementations for testing the Search API." core = 7.x package = Search -dependencies[] = search_api +dependencies[] = search_api:search_api files[] = search_api_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2013-01-09 -version = "7.x-1.4" +; Information added by Drupal.org packaging script on 2019-03-11 +version = "7.x-1.26" core = "7.x" project = "search_api" -datestamp = "1357726719" - +datestamp = "1552334832" diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test.install b/sites/all/modules/contrib/search/search_api/tests/search_api_test.install index f06f37d..2db73bb 100644 --- a/sites/all/modules/contrib/search/search_api/tests/search_api_test.install +++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test.install @@ -22,7 +22,7 @@ function search_api_test_schema() { 'description' => 'The title of the item.', 'type' => 'varchar', 'length' => 50, - 'not null' => TRUE, + 'not null' => FALSE, ), 'body' => array( 'description' => 'A text belonging to the item.', @@ -33,7 +33,19 @@ function search_api_test_schema() { 'description' => 'A string identifying the type of item.', 'type' => 'varchar', 'length' => 50, - 'not null' => TRUE, + 'not null' => FALSE, + ), + 'keywords' => array( + 'description' => 'A comma separated list of keywords.', + 'type' => 'varchar', + 'length' => 200, + '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/sites/all/modules/contrib/search/search_api/tests/search_api_test.module b/sites/all/modules/contrib/search/search_api/tests/search_api_test.module index 998598e..1b227f3 100644 --- a/sites/all/modules/contrib/search/search_api/tests/search_api_test.module +++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test.module @@ -1,5 +1,10 @@ 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, + ), + 'search_api_test/delete/%search_api_test' => array( + 'title' => 'Delete items', + 'page callback' => 'search_api_test_delete', 'page arguments' => array(2), 'access callback' => TRUE, ), @@ -43,6 +54,12 @@ function search_api_test_insert_item(array $form, array &$form_state) { 'type' => array( '#type' => 'textfield', ), + 'keywords' => array( + '#type' => 'textfield', + ), + 'prices' => array( + '#type' => 'textfield', + ), 'submit' => array( '#type' => 'submit', '#value' => t('Save'), @@ -55,7 +72,7 @@ function search_api_test_insert_item(array $form, array &$form_state) { */ function search_api_test_insert_item_submit(array $form, array &$form_state) { form_state_values_clean($form_state); - db_insert('search_api_test')->fields($form_state['values'])->execute(); + db_insert('search_api_test')->fields(array_filter($form_state['values']))->execute(); module_invoke_all('entity_insert', search_api_test_load($form_state['values']['id']), 'search_api_test'); } @@ -71,59 +88,22 @@ 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))); } /** - * 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) { - // Slight "hack" for testing complex queries. - if ($keys == '|COMPLEX|') { - $keys = array( - '#conjunction' => 'AND', - 'test', - array( - '#conjunction' => 'OR', - 'baz', - 'foobar', - ), - array( - '#conjunction' => 'AND', - '#negation' => TRUE, - 'bar', - ), - ); - } - $query = $index->query() - ->keys($keys) - ->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(); +function search_api_test_touch($entity) { + module_invoke_all('entity_update', $entity, 'search_api_test'); +} - $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; +/** + * 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'); } /** @@ -177,6 +157,18 @@ function search_api_test_entity_property_info() { 'description' => "The item's parent.", 'getter callback' => 'search_api_test_parent', ), + 'keywords' => array( + 'label' => 'Keywords', + 'type' => 'list', + '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; @@ -198,6 +190,24 @@ function search_api_test_parent($entity) { return search_api_test_load($entity->id - 1); } +/** + * List callback. + */ +function search_api_test_list_callback($data, array $options, $name) { + if (is_array($data)) { + $res = is_array($data[$name]) ? $data[$name] : explode(',', $data[$name]); + } + else { + $res = is_array($data->$name) ? $data->$name : explode(',', $data->$name); + } + if ($name == 'prices') { + foreach ($res as &$x) { + $x = (float) $x; + } + } + return array_filter($res); +} + /** * Implements hook_search_api_service_info(). */ @@ -215,6 +225,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( @@ -230,38 +245,72 @@ class SearchApiTestService extends SearchApiAbstractService { return $form; } - 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(); - foreach ($items as $id => $item) { - if ($id % 8) { - $ret[] = $id; - } - } - return $this->index($index, $ret); + /** + * {@inheritdoc} + */ + public function addIndex(SearchApiIndex $index) { + $this->checkErrorState(); } - protected function index(SearchApiIndex $index, array $ids) { + /** + * {@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(). + * + * 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) { + $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); + foreach ($items as $id => $item) { + if ($id == $exclude) { + unset($items[$id]); + } + } + $ids = array_keys($items); + $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) { + $this->checkErrorState(); if ($ids == 'all') { if ($index) { $this->options['indexes'][$index->machine_name] = array(); @@ -278,6 +327,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(); @@ -309,8 +364,16 @@ class SearchApiTestService extends SearchApiAbstractService { return $ret; } - public function fieldsUpdated(SearchApiIndex $index) { - return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0; + /** + * 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. + */ + protected function checkErrorState() { + if (variable_get('search_api_test_error_state', FALSE)) { + throw new SearchApiException(); + } } } diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_2.info b/sites/all/modules/contrib/search/search_api/tests/search_api_test_2.info new file mode 100644 index 0000000..5d36c8f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_2.info @@ -0,0 +1,16 @@ +name = Search API Test Service 2 +description = "A module providing a second test search service." +core = 7.x +package = Search + +dependencies[] = search_api:search_api + +files[] = search_api_test_service_2.module + +hidden = TRUE + +; Information added by Drupal.org packaging script on 2019-03-11 +version = "7.x-1.26" +core = "7.x" +project = "search_api" +datestamp = "1552334832" diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_2.module b/sites/all/modules/contrib/search/search_api/tests/search_api_test_2.module new file mode 100644 index 0000000..d78a80e --- /dev/null +++ b/sites/all/modules/contrib/search/search_api/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(); + } +}